diff --git a/include/mrdox/Support/Handlebars.hpp b/include/mrdox/Support/Handlebars.hpp index 17683c4a6..15c84d490 100644 --- a/include/mrdox/Support/Handlebars.hpp +++ b/include/mrdox/Support/Handlebars.hpp @@ -55,6 +55,20 @@ struct HandlebarsOptions */ bool ignoreStandalone = false; + /** Disables implicit context for partials + + When enabled, partials that are not passed a context value will + execute against an empty object. + */ + bool explicitPartialContext = false; + + /** Enable recursive field lookup + + When enabled, fields will be looked up recursively in objects + and arrays. + */ + bool compat = false; + /** Custom private data object This variable can be used to pass in an object to define custom @@ -106,6 +120,9 @@ namespace detail { using partials_map = std::unordered_map< std::string, std::string, string_hash, std::equal_to<>>; + + using partials_view_map = std::unordered_map< + std::string, std::string_view, string_hash, std::equal_to<>>; } /** Reference to output stream used by handlebars @@ -124,8 +141,8 @@ class MRDOX_DECL OutputRef using fptr = void (*)(void * out, std::string_view sv); void * out_; fptr fptr_; + std::size_t indent_ = 0; -private: template static void @@ -164,6 +181,9 @@ class MRDOX_DECL OutputRef , fptr_( &noop_output ) {} + OutputRef& + write_impl( std::string_view sv ); + public: /** Constructor for std::string output @@ -209,8 +229,7 @@ class MRDOX_DECL OutputRef OutputRef& operator<<( std::string_view sv ) { - fptr_( out_, sv ); - return *this; + return write_impl( sv ); } /** Write to output @@ -221,8 +240,7 @@ class MRDOX_DECL OutputRef OutputRef& operator<<( char c ) { - fptr_( out_, std::string_view( &c, 1 ) ); - return *this; + return write_impl( std::string_view( &c, 1 ) ); } /** Write to output @@ -233,8 +251,7 @@ class MRDOX_DECL OutputRef OutputRef& operator<<( char const * c ) { - fptr_( out_, std::string_view( c ) ); - return *this; + return write_impl( std::string_view( c ) ); } /** Write to output @@ -248,8 +265,19 @@ class MRDOX_DECL OutputRef operator<<( T v ) { std::string s = fmt::format( "{}", v ); - fptr_( out_, s ); - return *this; + return write_impl( s ); + } + + void + setIndent(std::size_t indent) + { + indent_ = indent; + } + + std::size_t + getIndent() noexcept + { + return indent_; } }; @@ -842,9 +870,16 @@ class Handlebars { std::string render( std::string_view templateText, - dom::Value const& context = {}, + dom::Value const& context, HandlebarsOptions const& options = {}) const; + std::string + render(std::string_view templateText) const + { + dom::Object const& context = {}; + return render(templateText, context); + } + /** Render a handlebars template This function renders the specified handlebars template and @@ -1136,6 +1171,7 @@ class Handlebars { Handlebars::Tag const& tag, OutputRef &out, dom::Value const& context, + HandlebarsOptions const& opt, detail::RenderState& state) const; void @@ -1152,17 +1188,24 @@ class Handlebars { dom::Value const& context, detail::RenderState& state, dom::Array &args, - HandlebarsCallback& opt) const; + HandlebarsCallback& cb, + HandlebarsOptions const& opt) const; std::pair evalExpr( dom::Value const &context, std::string_view expression, detail::RenderState &state, + HandlebarsOptions const& opt, bool evalLiterals) const; std::pair getHelper(std::string_view name, bool isBlock) const; + + std::pair + getPartial( + std::string_view name, + detail::RenderState const& state) const; }; /** Determine if a value is truthy diff --git a/share/gdb/README.adoc b/share/gdb/README.adoc new file mode 100644 index 000000000..c7b7b2981 --- /dev/null +++ b/share/gdb/README.adoc @@ -0,0 +1,24 @@ += GDB pretty printers + +Create or modify your `.gdbinit` file to contain the following: + +[source,python] +---- +python +import sys +sys.path.insert(0, '/path/share/gdb') <1> +from mrdox_printers import register_mrdox_printers <2> +register_mrdox_printers() <3> +end +---- + +<1> Make GDB see the directory with the printers +<2> Import the function that registers the printers with GDB +<3> Effectively register the printers + +Note that this pattern does not register the printers unless the user explicitly asks for it by calling the `register_mrdox_printers` function. +This helps the scripts separate these concerns. + +NOTE: The printers require Python 3 + + diff --git a/share/gdb/__init__.py b/share/gdb/__init__.py new file mode 100644 index 000000000..bb7b160de --- /dev/null +++ b/share/gdb/__init__.py @@ -0,0 +1 @@ +# Intentionally empty diff --git a/share/gdb/mrdox_printers.py b/share/gdb/mrdox_printers.py new file mode 100644 index 000000000..cc5385bda --- /dev/null +++ b/share/gdb/mrdox_printers.py @@ -0,0 +1,116 @@ +# +# Copyright (c) 2023 alandefreitas (alandefreitas@gmail.com) +# +# Distributed under the Boost Software License, Version 1.0. +# https://www.boost.org/LICENSE_1_0.txt +# + +import gdb + + +class utils: + @staticmethod + def resolve_type(t): + if t.code == gdb.TYPE_CODE_REF: + t = t.target() + t = t.unqualified().strip_typedefs() + typename = t.tag + if typename is None: + return None + return t + + @staticmethod + def resolved_typename(val): + t = val.type + t = utils.resolve_type(t) + if t is not None: + return str(t) + else: + return str(val.type) + + @staticmethod + def pct_decode(s: str): + return urllib.parse.unquote(s) + + sv_pool = [] + + @staticmethod + def make_string_view(cstr: gdb.Value, n: int): + sv_ptr: gdb.Value = gdb.parse_and_eval('malloc(sizeof(class boost::core::basic_string_view))') + sv_ptr_str = cstr.format_string(format='x') + gdb.execute( + f'call ((boost::core::basic_string_view*){sv_ptr})->basic_string_view((const char*){sv_ptr_str}, {n})', + to_string=True) + sv = gdb.parse_and_eval(f'*((boost::core::basic_string_view*){sv_ptr})') + copy: gdb.Value = sv + utils.sv_pool.append(sv_ptr) + if len(utils.sv_pool) > 5000: + gdb.execute(f'call free({utils.sv_pool[0]})', to_string=True) + utils.sv_pool.pop(0) + return copy + + pct_sv_pool = [] + + @staticmethod + def make_pct_string_view(cstr: gdb.Value, n: int): + sv_ptr: gdb.Value = gdb.parse_and_eval('malloc(sizeof(class boost::urls::pct_string_view))') + sv_ptr_str = cstr.format_string(format='x') + gdb.execute( + f'call ((boost::urls::pct_string_view*){sv_ptr})->pct_string_view((const char*){sv_ptr_str}, {n})', + to_string=True) + sv = gdb.parse_and_eval(f'*((boost::urls::pct_string_view*){sv_ptr})') + copy: gdb.Value = sv + utils.pct_sv_pool.append(sv_ptr) + if len(utils.sv_pool) > 5000: + gdb.execute(f'call free({utils.pct_sv_pool[0]})', to_string=True) + utils.sv_pool.pop(0) + return copy + + +class DomValuePrinter: + def __init__(self, value): + self.value = value + + def children(self): + # Get kind enum + kind = self.value['kind_'] + if kind == 1: + yield 'Boolean', self.value['b_'] + elif kind == 2: + yield 'Integer', self.value['i_'] + elif kind == 3: + yield 'String', self.value['str_'] + elif kind == 4: + yield 'Array', self.value['arr_'] + elif kind == 5: + yield 'Object', self.value['obj_']['impl_']['_M_ptr'] + elif kind == 6: + yield 'Function', self.value['fn_'] + else: + yield 'kind_', self.value['kind_'] + + +class EnumPrinter: + def __init__(self, value): + self.value = value + + def to_string(self): + s: str = self.value.format_string(raw=True) + return s.rsplit(':', 1)[-1] + + +if __name__ != "__main__": + def lookup_function(val: gdb.Value): + if val.type.code == gdb.TYPE_CODE_ENUM: + return EnumPrinter(val) + + typename: str = utils.resolved_typename(val) + if typename == 'clang::mrdox::dom::Value': + return DomValuePrinter(val) + return None + + +def register_mrdox_printers(obj=None): + if obj is None: + obj = gdb + obj.pretty_printers.append(lookup_function) diff --git a/src/lib/Support/Handlebars.cpp b/src/lib/Support/Handlebars.cpp index 27ca8975c..44b2aa258 100644 --- a/src/lib/Support/Handlebars.cpp +++ b/src/lib/Support/Handlebars.cpp @@ -23,6 +23,46 @@ namespace clang { namespace mrdox { +// ============================================================== +// Output +// ============================================================== +OutputRef& +OutputRef:: +write_impl( std::string_view sv ) +{ + if (indent_ == 0) + { + fptr_( out_, sv ); + return *this; + } + + std::size_t pos = sv.find('\n'); + if (pos == std::string_view::npos) + { + fptr_( out_, sv ); + return *this; + } + + fptr_( out_, sv.substr(0, pos + 1) ); + ++pos; + while (pos < sv.size()) + { + for (std::size_t i = 0; i < indent_; ++i) + { + fptr_( out_, std::string_view(" ") ); + } + std::size_t next = sv.find('\n', pos); + if (next == std::string_view::npos) + { + fptr_( out_, sv.substr(pos) ); + return *this; + } + fptr_( out_, sv.substr(pos, next - pos + 1) ); + pos = next + 1; + } + return *this; +} + // ============================================================== // Utility functions // ============================================================== @@ -38,9 +78,8 @@ isTruthy(dom::Value const& arg) case dom::Kind::String: return !arg.getString().empty(); case dom::Kind::Array: - return !arg.getArray().empty(); case dom::Kind::Object: - return !arg.getObject().empty(); + return true; case dom::Kind::Null: return false; default: @@ -53,8 +92,6 @@ isEmpty(dom::Value const& arg) { if (arg.isArray()) return arg.getArray().empty(); - if (arg.isObject()) - return arg.getObject().empty(); if (arg.isInteger()) return false; return !isTruthy(arg); @@ -284,9 +321,12 @@ namespace detail { { std::string_view templateText0; std::string_view templateText; - detail::partials_map inlinePartials; + std::vector inlinePartials; + std::vector partialBlocks; + std::size_t partialBlockLevel = 0; dom::Object data; dom::Object blockValues; + std::vector compatStack; }; } @@ -452,6 +492,39 @@ popFirstSegment(std::string_view& path0) return path.substr(0, pos); } +struct position_in_text +{ + std::size_t line = std::size_t(-1); + std::size_t column = std::size_t(-1); + std::size_t pos = std::size_t(-1); + + constexpr + operator bool() const + { + return line != std::size_t(-1); + } +}; + +constexpr +position_in_text +find_position_in_text( + std::string_view text, + std::string_view substr) +{ + position_in_text res; + if (substr.data() >= text.data() && + substr.data() <= text.data() + text.size()) + { + res.pos = substr.data() - text.data(); + res.line = std::ranges::count(text.substr(0, res.pos), '\n') + 1; + if (res.line == 1) + res.column = res.pos; + else + res.column = res.pos - text.rfind('\n', res.pos); + } + return res; +} + void checkPath(std::string_view path0, detail::RenderState const& state) { @@ -464,19 +537,10 @@ checkPath(std::string_view path0, detail::RenderState const& state) 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()) + auto res = find_position_in_text(state.templateText0, path0); + if (res) { - 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, res.line, res.column, res.pos); } throw HandlebarsError(msg); } @@ -1028,6 +1092,8 @@ render_to( { state.data = options.data.getObject(); } + state.inlinePartials.emplace_back(); + state.compatStack.emplace_back(context); render_to(out, context, options, state); } @@ -1281,9 +1347,11 @@ popContextSegment(std::string_view& contextPath) { if (pos == std::string_view::npos) { contextPath.remove_prefix(contextPath.size()); - return false; } - contextPath = contextPath.substr(0, pos); + else + { + contextPath = contextPath.substr(0, pos); + } return true; } @@ -1294,6 +1362,7 @@ evalExpr( dom::Value const & context, std::string_view expression, detail::RenderState& state, + HandlebarsOptions const& opt, bool evalLiterals) const { expression = trim_spaces(expression); @@ -1341,7 +1410,7 @@ evalExpr( HandlebarsCallback cb; cb.name_ = helper; cb.context_ = &context; - setupArgs(all, context, state, args, cb); + setupArgs(all, context, state, args, cb, opt); return {fn(args, cb).first, true}; } } @@ -1355,6 +1424,19 @@ evalExpr( if (!contextPathV.isString()) return {nullptr, false}; std::string_view 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); + } + } + } while (expression.starts_with("..")) { if (!popContextSegment(contextPath)) return {nullptr, false}; @@ -1370,8 +1452,11 @@ evalExpr( dom::Value root = state.data.find("root"); do { absContextPath = contextPath; - absContextPath += '.'; - absContextPath += expression; + if (!expression.empty()) + { + absContextPath += '.'; + absContextPath += expression; + } auto [v, defined] = lookupPropertyImpl(root, absContextPath, state); if (defined) { return {v, defined}; @@ -1380,18 +1465,38 @@ evalExpr( } while (!contextPath.empty()); return lookupPropertyImpl(root, expression, state); } - auto [cv, defined] = lookupPropertyImpl(context, expression, state); + auto [r, defined] = lookupPropertyImpl(context, expression, state); if (defined) { - return {cv, defined}; + return {r, defined}; + } + std::tie(r, defined) = lookupPropertyImpl(state.blockValues, expression, state); + if (defined) + { + return {r, defined}; } - return lookupPropertyImpl(state.blockValues, expression, state); + if (opt.compat) + { + auto parentContexts = std::ranges::views::reverse(state.compatStack); + for (auto parentContext: parentContexts) + { + std::tie(r, defined) = lookupPropertyImpl(parentContext, expression, state); + if (defined) + { + return {r, defined}; + } + } + } + return {nullptr, false}; } auto Handlebars:: -getHelper(std::string_view helper, bool isNoArgBlock) const -> std::pair { +getHelper(std::string_view helper, bool isNoArgBlock) const + -> std::pair +{ auto it = helpers_.find(helper); - if (it != helpers_.end()) { + if (it != helpers_.end()) + { return {it->second, true}; } helper = !isNoArgBlock ? "helperMissing" : "blockHelperMissing"; @@ -1400,6 +1505,42 @@ getHelper(std::string_view helper, bool isNoArgBlock) const -> std::pairsecond, false}; } +auto +Handlebars:: +getPartial( + std::string_view name, + detail::RenderState const& state) const + -> std::pair +{ + // Inline partials + auto blockPartials = std::ranges::views::reverse(state.inlinePartials); + for (auto blockInlinePartials: blockPartials) + { + auto it = blockInlinePartials.find(name); + if (it != blockInlinePartials.end()) + { + return {it->second, true}; + } + } + + // Main partials + auto it = this->partials_.find(name); + if (it != this->partials_.end()) + { + return {it->second, true}; + } + + // Partial block + if (name == "@partial-block") + { + return { + state.partialBlocks[state.partialBlockLevel - 1], + true}; + } + + return { {}, false }; +} + // Parse a block starting at templateText bool parseBlock( @@ -1537,7 +1678,7 @@ renderTag( } else if (tag.type == '*') { - renderDecorator(tag, out, context, state); + renderDecorator(tag, out, context, opt, state); } else if (tag.type != '/' && tag.type != '!') { @@ -1574,7 +1715,7 @@ renderExpression( cb.context_ = &context; cb.data_ = &state.data; cb.logger_ = &logger_; - setupArgs(tag.arguments, context, state, args, cb); + setupArgs(tag.arguments, context, state, args, cb, opt); auto [res, render] = fn(args, cb); if (render == HelperBehavior::RENDER_RESULT) { format_to(out, res, opt2); @@ -1596,7 +1737,7 @@ renderExpression( unescaped = unescapeString(helper_expr); helper_expr = unescaped; } - auto [v, defined] = evalExpr(context, helper_expr, state, false); + auto [v, defined] = evalExpr(context, helper_expr, state, opt, false); if (defined) { format_to(out, v, opt2); @@ -1608,7 +1749,7 @@ renderExpression( dom::Array args = dom::newArray(); HandlebarsCallback cb; cb.name_ = helper_expr; - setupArgs(tag.arguments, context, state, args, cb); + setupArgs(tag.arguments, context, state, args, cb, opt); auto [res, render] = fn(args, cb); if (render == HelperBehavior::RENDER_RESULT) { format_to(out, res, opt2); @@ -1628,7 +1769,8 @@ setupArgs( dom::Value const& context, detail::RenderState & state, dom::Array &args, - HandlebarsCallback& cb) const + HandlebarsCallback& cb, + HandlebarsOptions const& opt) const { std::string_view expr; while (findExpr(expr, expression)) @@ -1637,12 +1779,12 @@ setupArgs( auto [k, v] = findKeyValuePair(expr); if (k.empty()) { - args.emplace_back(evalExpr(context, expr, state, true).first); + args.emplace_back(evalExpr(context, expr, state, opt, true).first); cb.ids_.push_back(expr); } else { - cb.hashes_.set(k, evalExpr(context, v, state, true).first); + cb.hashes_.set(k, evalExpr(context, v, state, opt, true).first); } } cb.renderState_ = &state; @@ -1654,6 +1796,7 @@ renderDecorator( Handlebars::Tag const& tag, OutputRef &out, dom::Value const& context, + HandlebarsOptions const& opt, detail::RenderState& state) const { // Validate decorator if (tag.helper != "inline") @@ -1665,7 +1808,7 @@ renderDecorator( // Evaluate expression std::string_view expr; findExpr(expr, tag.arguments); - auto [value, defined] = evalExpr(context, expr, state, true); + auto [value, defined] = evalExpr(context, expr, state, opt, true); if (!value.isString()) { out << fmt::format(R"([invalid decorator expression "{}" in "{}"])", tag.arguments, tag.buffer); @@ -1683,7 +1826,7 @@ renderDecorator( } } fnBlock = trim_rspaces(fnBlock); - state.inlinePartials[std::string(partial_name)] = std::string(fnBlock); + state.inlinePartials.back()[std::string(partial_name)] = fnBlock; } void @@ -1693,122 +1836,246 @@ renderPartial( OutputRef &out, dom::Value const &context, HandlebarsOptions const& opt, - detail::RenderState& state) const { - // Evaluate dynamic partial - std::string helper(tag.helper); - if (helper.starts_with('(')) { + detail::RenderState& state) const +{ + // ============================================================== + // Evaluate partial name + // ============================================================== + std::string partialName(tag.helper); + bool const isDynamicPartial = partialName.starts_with('('); + bool const isEscapedPartialName = + !partialName.empty() && + partialName.front() == '[' && + partialName.back() == ']'; + if (isDynamicPartial) + { std::string_view expr; - findExpr(expr, helper); - auto [value, defined] = evalExpr(context, expr, state, true); - if (value.isString()) { - helper = value.getString(); + findExpr(expr, partialName); + auto [value, defined] = evalExpr(context, expr, state, opt, true); + if (value.isString()) + { + partialName = value.getString(); } } + else if (isEscapedPartialName) + { + partialName = partialName.substr(1, partialName.size() - 2); + } + else if (is_literal_string(partialName)) + { + partialName = unescapeString(partialName); + } - // Parse block + // ============================================================== + // Parse Block + // ============================================================== std::string_view fnBlock; std::string_view inverseBlock; Tag inverseTag; - if (tag.type2 == '#') { - if (!parseBlock(tag.helper, tag, state.templateText, out, fnBlock, inverseBlock, inverseTag)) { + if (tag.type2 == '#') + { + if (!parseBlock(tag.helper, tag, state.templateText, out, fnBlock, inverseBlock, inverseTag)) + { return; } } - // Partial - auto it = this->partials_.find(helper); - std::string_view partial_content; - if (it != this->partials_.end()) - { - partial_content = it->second; - } - else + // ============================================================== + // Find partial content + // ============================================================== + // Look for registered partial + auto [partial_content, found] = getPartial(partialName, state); + if (!found) { - it = state.inlinePartials.find(helper); - if (it == state.inlinePartials.end()) { - if (tag.type2 == '#') { - partial_content = fnBlock; - } else { - throw HandlebarsError(fmt::format("The partial {} could not be found", helper)); - } - } else { - partial_content = it->second; + if (tag.type2 == '#') + { + partial_content = fnBlock; + } + else + { + throw HandlebarsError(fmt::format("The partial {} could not be found", + partialName)); } } - if (tag.arguments.empty()) + // ============================================================== + // Evaluate partial block to extract inline partials + // ============================================================== + if (tag.type2 == '#') { - if (tag.type2 == '#') { - // evaluate fnBlock to potentially populate extra partials - OutputRef dumb{}; - 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 + state.inlinePartials.emplace_back(); + OutputRef dumb{}; std::string_view templateText = state.templateText; - state.templateText = partial_content; - this->render_to(out, context, opt, state); + state.templateText = fnBlock; + this->render_to(dumb, context, opt, state); state.templateText = templateText; - if (tag.type2 == '#') { - state.inlinePartials.erase("@partial-block"); + } + + // ============================================================== + // Set @partial-block + // ============================================================== + if (tag.type2 == '#') + { + state.partialBlocks.emplace_back(fnBlock); + ++state.partialBlockLevel; + } + + // ============================================================== + // Setup partial context + // ============================================================== + // Default context + dom::Value partialCtx = dom::Object{}; + if (!opt.explicitPartialContext) + { + if (context.isObject()) + { + partialCtx = createFrame(context.getObject()); + } + else + { + partialCtx = context; } } - else + + // Populate with arguments + if (!tag.arguments.empty()) { // create context from specified keys auto tagContent = tag.arguments; - dom::Object partialCtx; + bool partialCtxDefined = false; std::string_view expr; while (findExpr(expr, tagContent)) { tagContent = tagContent.substr(expr.data() + expr.size() - tagContent.data()); auto [partialKey, contextKey] = findKeyValuePair(expr); - if (partialKey.empty()) + bool const isContextReplacement = partialKey.empty(); + if (isContextReplacement) { - auto [value, defined] = evalExpr(context, expr, state, true); - if (defined && value.isObject()) + // Check if context has been replaced before + if (partialCtxDefined) + { + std::size_t n = 2; + while (findExpr(expr, tagContent)) + { + auto [partialKey2, _] = findKeyValuePair(expr); + if (!partialKey2.empty()) { + break; + } + ++n; + } + std::string msg = fmt::format( + "Unsupported number of partial arguments: {}", n); + auto res = find_position_in_text(state.templateText0, tag.buffer); + if (res) + { + throw HandlebarsError(msg, res.line, res.column, res.pos); + } + throw HandlebarsError(msg); + } + + // Replace context + auto [value, defined] = evalExpr(context, expr, state, opt, true); + if (defined) { - partialCtx = createFrame(value.getObject()); + if (value.isObject()) + { + partialCtx = createFrame(value.getObject()); + } + else + { + partialCtx = value; + } } + partialCtxDefined = true; continue; } - if (contextKey != ".") { - auto [value, defined] = evalExpr(context, contextKey, state, true); - if (defined) + + // Argument is key=value pair + dom::Value value; + bool defined; + if (contextKey != ".") + { + std::tie(value, defined) = evalExpr(context, contextKey, state, opt, true); + } + else + { + value = context; + defined = true; + } + if (defined) + { + bool const needs_reset_context = !partialCtx.isObject(); + if (needs_reset_context) { - partialCtx.set(partialKey, value); + if (!opt.explicitPartialContext && + context.isObject()) + { + partialCtx = createFrame(context.getObject()); + } + else + { + partialCtx = dom::Object{}; + } } - } else { - partialCtx.set(partialKey, context); + partialCtx.getObject().set(partialKey, value); } } - std::string_view templateText = state.templateText; - state.templateText = partial_content; - this->render_to(out, partialCtx, opt, state); - state.templateText = templateText; } - if (tag.removeRWhitespace) { + // ============================================================== + // Determine if partial is standalone + // ============================================================== + auto beforePartial = state.templateText0.substr( + 0, tag.buffer.data() - state.templateText0.data()); + std::string_view lastLine = beforePartial; + auto pos = beforePartial.find_last_of("\r\n"); + if (pos != std::string_view::npos) + { + lastLine = beforePartial.substr(pos + 1); + } + bool const isStandalone = std::ranges::all_of( + lastLine, [](char c) { return c == ' '; }); + std::size_t const partialIndent = + !opt.preventIndent && isStandalone ? lastLine.size() : 0; + + // ============================================================== + // Render partial + // ============================================================== + std::string_view templateText = state.templateText; + state.templateText = partial_content; + bool const isPartialBlock = partialName == "@partial-block"; + state.partialBlockLevel -= isPartialBlock; + out.setIndent(out.getIndent() + partialIndent); + state.compatStack.emplace_back(context); + this->render_to(out, partialCtx, opt, state); + state.compatStack.pop_back(); + out.setIndent(out.getIndent() - partialIndent); + state.partialBlockLevel += isPartialBlock; + state.templateText = templateText; + + if (tag.type2 == '#') + { + state.inlinePartials.pop_back(); + state.partialBlocks.pop_back(); + --state.partialBlockLevel; + } + + // ============================================================== + // Remove standalone whitespace + // ============================================================== + if (tag.removeRWhitespace) + { state.templateText = trim_lspaces(state.templateText); } else if (!opt.ignoreStandalone) { - auto beforePartial = state.templateText0.substr( - 0, tag.buffer.data() - state.templateText0.data()); - std::string_view lastLine = beforePartial; - auto pos = beforePartial.find_last_of("\r\n"); - if (pos != std::string_view::npos) - { - lastLine = beforePartial.substr(pos + 1); - } - bool const isStandalone = std::ranges::all_of( - lastLine, [](char c) { return c == ' '; }); if (isStandalone) { state.templateText = trim_ldelimiters(state.templateText, " "); + if (state.templateText.starts_with('\n')) + { + state.templateText.remove_prefix(1); + } } } } @@ -1822,12 +2089,13 @@ renderBlock( dom::Value const& context, HandlebarsOptions const& opt, detail::RenderState& state) const { - // Opening a section tag - // Find closing tag if (tag.removeRWhitespace) { state.templateText = trim_lspaces(state.templateText); } + // ============================================================== + // Parse block + // ============================================================== std::string_view fnBlock; std::string_view inverseBlock; Tag inverseTag; @@ -1835,6 +2103,9 @@ renderBlock( return; } + // ============================================================== + // Setup helper parameters + // ============================================================== dom::Array args = dom::newArray(); HandlebarsCallback cb; cb.name_ = tag.helper; @@ -1858,9 +2129,11 @@ renderBlock( arguments = tag.helper; } } - setupArgs(arguments, context, state, args, cb); + setupArgs(arguments, context, state, args, cb, opt); - // Setup block params + // ============================================================== + // Setup block parameters + // ============================================================== std::string_view expr; std::string_view bps = tag.blockParams; while (findExpr(expr, bps)) @@ -1869,7 +2142,9 @@ renderBlock( cb.blockParams_.push_back(expr); } - // Setup callback functions + // ============================================================== + // Setup callbacks + // ============================================================== if (!tag.rawBlock) { cb.fn_ = [this, fnBlock, opt, &state]( OutputRef os, @@ -1946,7 +2221,11 @@ renderBlock( }; } + // ============================================================== // Call helper + // ============================================================== + state.inlinePartials.emplace_back(); + state.compatStack.emplace_back(context); auto [res, render] = fn(args, cb); if (render == HelperBehavior::RENDER_RESULT) { format_to(out, res, opt); @@ -1955,6 +2234,8 @@ renderBlock( opt2.noEscape = true; format_to(out, res, opt2); } + state.inlinePartials.pop_back(); + state.compatStack.pop_back(); } void @@ -1963,6 +2244,9 @@ registerPartial( std::string_view name, std::string_view text) { + auto it = partials_.find(name); + if (it != partials_.end()) + partials_.erase(it); partials_.emplace(std::string(name), std::string(text)); } diff --git a/src/test/lib/Support/Handlebars.cpp b/src/test/lib/Support/Handlebars.cpp index d1cde383b..f3503e319 100644 --- a/src/test/lib/Support/Handlebars.cpp +++ b/src/test/lib/Support/Handlebars.cpp @@ -1279,6 +1279,607 @@ whitespace_control() } } +void +partials() +{ + // https://github.com/handlebars-lang/handlebars.js/blob/4.x/spec/partials.js + + Handlebars hbs; + HandlebarsOptions emptyDataOptions; + emptyDataOptions.data = false; + + dom::Object hash; + dom::Array dudes; + dom::Object dude1; + dude1.set("name", "Yehuda"); + dude1.set("url", "http://yehuda"); + dudes.emplace_back(dude1); + dom::Object dude2; + dude2.set("name", "Alan"); + dude2.set("url", "http://alan"); + dudes.emplace_back(dude2); + hash.set("dudes", dudes); + + // basic partials + { + std::string str = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}"; + std::string partial = "{{name}} ({{url}}) "; + + hbs.registerPartial("dude", partial); + BOOST_TEST(hbs.render(str, hash) == "Dudes: Yehuda (http://yehuda) Alan (http://alan) "); + BOOST_TEST(hbs.render(str, hash, emptyDataOptions) == "Dudes: Yehuda (http://yehuda) Alan (http://alan) "); + } + + // dynamic partials + { + std::string str = "Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}"; + std::string partial = "{{name}} ({{url}}) "; + hbs.registerHelper("partial", []() { + return "dude"; + }); + hbs.registerPartial("dude", partial); + BOOST_TEST(hbs.render(str, hash) == "Dudes: Yehuda (http://yehuda) Alan (http://alan) "); + BOOST_TEST(hbs.render(str, hash, emptyDataOptions) == "Dudes: Yehuda (http://yehuda) Alan (http://alan) "); + hbs.unregisterPartial("dude"); + } + + // failing dynamic partials + { + std::string str = "Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}"; + std::string partial = "{{name}} ({{url}}) "; + hbs.registerHelper("partial", []() { + return "missing"; + }); + hbs.registerPartial("dude", partial); + BOOST_TEST_THROW_WITH( + hbs.render(str, hash), + HandlebarsError, "The partial missing could not be found"); + } + + // partials with context + { + // Partials can be passed a context + std::string str = "Dudes: {{>dude dudes}}"; + hbs.registerPartial("dude", "{{#this}}{{name}} ({{url}}) {{/this}}"); + BOOST_TEST(hbs.render(str, hash) == "Dudes: Yehuda (http://yehuda) Alan (http://alan) "); + } + + // partials with no context + { + hbs.registerPartial("dude", "{{name}} ({{url}}) "); + HandlebarsOptions opt2; + opt2.explicitPartialContext = true; + BOOST_TEST(hbs.render("Dudes: {{#dudes}}{{>dude}}{{/dudes}}", hash, opt2) == "Dudes: () () "); + BOOST_TEST(hbs.render("Dudes: {{#dudes}}{{>dude name=\"foo\"}}{{/dudes}}", hash, opt2) == "Dudes: foo () foo () "); + } + + // partials with string context + { + hbs.registerPartial("dude", "{{.}}"); + BOOST_TEST(hbs.render("Dudes: {{>dude \"dudes\"}}") == "Dudes: dudes"); + } + + // partials with undefined context + { + hbs.registerPartial("dude", "{{foo}} Empty"); + BOOST_TEST(hbs.render("Dudes: {{>dude dudes}}") == "Dudes: Empty"); + } + + // partials with duplicate parameters + { + BOOST_TEST_THROW_WITH( + hbs.render("Dudes: {{>dude dudes foo bar=baz}}"), + HandlebarsError, + "Unsupported number of partial arguments: 2 - 1:7"); + } + + // partials with parameters + { + // Basic partials output based on current context. + hash.set("foo", "bar"); + hbs.registerPartial("dude", "{{others.foo}}{{name}} ({{url}}) "); + BOOST_TEST( + hbs.render("Dudes: {{#dudes}}{{> dude others=..}}{{/dudes}}", hash) == + "Dudes: barYehuda (http://yehuda) barAlan (http://alan) "); + } + + // partial in a partial + { + hbs.registerPartial("dude", "{{name}} {{> url}} "); + hbs.registerPartial("url", "{{url}}"); + BOOST_TEST( + hbs.render("Dudes: {{#dudes}}{{>dude}}{{/dudes}}", hash) == + "Dudes: Yehuda http://yehuda Alan http://alan "); + } + + // rendering undefined partial throws an exception + { + BOOST_TEST_THROW_WITH( + hbs.render("{{> whatever}}"), + HandlebarsError, "The partial whatever could not be found"); + } + + // registering undefined partial throws an exception + { + // Nothing to test since this is a type error in C++. + } + + // rendering function partial in vm mode + { + // Unsupported by this implementation. + } + + // a partial preceding a selector + { + // Regular selectors can follow a partial + dom::Object ctx; + ctx.set("name", "Jeepers"); + ctx.set("anotherDude", "Creepers"); + hbs.registerPartial("dude", "{{name}}"); + BOOST_TEST( + hbs.render("Dudes: {{>dude}} {{anotherDude}}", ctx) == + "Dudes: Jeepers Creepers"); + } + + // Partials with slash paths + { + dom::Object ctx; + ctx.set("name", "Jeepers"); + ctx.set("anotherDude", "Creepers"); + hbs.registerPartial("shared/dude", "{{name}}"); + BOOST_TEST( + hbs.render("Dudes: {{> shared/dude}}", ctx) == + "Dudes: Jeepers"); + } + + // Partials with slash and point paths + { + dom::Object ctx; + ctx.set("name", "Jeepers"); + ctx.set("anotherDude", "Creepers"); + hbs.registerPartial("shared/dude.thing", "{{name}}"); + BOOST_TEST( + hbs.render("Dudes: {{> shared/dude.thing}}", ctx) == + "Dudes: Jeepers"); + } + + // Global Partials + { + // There's no global environment in this implementation + dom::Object ctx; + ctx.set("name", "Jeepers"); + ctx.set("anotherDude", "Creepers"); + hbs.registerPartial("shared/dude", "{{name}}"); + hbs.registerPartial("globalTest", "{{anotherDude}}"); + BOOST_TEST( + hbs.render("Dudes: {{> shared/dude}} {{> globalTest}}", ctx) == + "Dudes: Jeepers Creepers"); + } + + // Multiple partial registration + { + // This feature is not supported by this implementation. + } + + // Partials with integer path + { + dom::Object ctx; + ctx.set("name", "Jeepers"); + ctx.set("anotherDude", "Creepers"); + hbs.registerPartial("404", "{{name}}"); + BOOST_TEST( + hbs.render("Dudes: {{> 404}}", ctx) == + "Dudes: Jeepers"); + } + + // Partials with complex path + { + dom::Object ctx; + ctx.set("name", "Jeepers"); + ctx.set("anotherDude", "Creepers"); + hbs.registerPartial("404/asdf?.bar", "{{name}}"); + BOOST_TEST( + hbs.render("Dudes: {{> 404/asdf?.bar}}", ctx) == + "Dudes: Jeepers"); + } + + // Partials with string + { + dom::Object ctx; + ctx.set("name", "Jeepers"); + ctx.set("anotherDude", "Creepers"); + hbs.registerPartial("+404/asdf?.bar", "{{name}}"); + BOOST_TEST( + hbs.render("Dudes: {{> '+404/asdf?.bar'}}", ctx) == + "Dudes: Jeepers"); + } + + // should handle empty partial + { + hbs.registerPartial("dude", ""); + BOOST_TEST( + hbs.render("Dudes: {{#dudes}}{{> dude}}{{/dudes}}", hash) == + "Dudes: "); + } + + // throw on missing partial + { + hbs.unregisterPartial("dude"); + BOOST_TEST_THROW_WITH( + hbs.render("{{> dude}}", hash), + HandlebarsError, "The partial dude could not be found"); + } +} + +void +partial_blocks() +{ + // https://github.com/handlebars-lang/handlebars.js/blob/4.x/spec/partials.js + + Handlebars hbs; + + // should render partial block as default + { + BOOST_TEST(hbs.render("{{#> dude}}success{{/dude}}") == "success"); + } + + // should execute default block with proper context + { + dom::Object context; + context.set("value", "success"); + BOOST_TEST(hbs.render("{{#> dude context}}{{value}}{{/dude}}", context) == "success"); + } + + // should propagate block parameters to default block + { + dom::Object context; + dom::Object value; + value.set("value", "success"); + context.set("context", value); + BOOST_TEST(hbs.render("{{#with context as |me|}}{{#> dude}}{{me.value}}{{/dude}}{{/with}}", context) == "success"); + } + + // should not use partial block if partial exists + { + hbs.registerPartial("dude", "success"); + BOOST_TEST(hbs.render("{{#> dude}}fail{{/dude}}") == "success"); + } + + // should render block from partial + { + hbs.registerPartial("dude", "{{> @partial-block }}"); + BOOST_TEST(hbs.render("{{#> dude}}success{{/dude}}") == "success"); + } + + // should be able to render the partial-block twice + { + hbs.registerPartial("dude", "{{> @partial-block }} {{> @partial-block }}"); + BOOST_TEST(hbs.render("{{#> dude}}success{{/dude}}") == "success success"); + } + + // should render block from partial with context + { + 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}}{{value}}{{/dude}}", ctx) == "success"); + } + + // should be able to access the @data frame from a partial-block + { + dom::Object ctx; + ctx.set("value", "success"); + hbs.registerPartial("dude", "before-block: {{@root/value}} {{> @partial-block }}"); + BOOST_TEST( + hbs.render("{{#> dude}}in-block: {{@root/value}}{{/dude}}", ctx) == + "before-block: success in-block: success"); + } + + // should allow the #each-helper to be used along with partial-blocks + { + dom::Object ctx; + dom::Array values; + values.emplace_back("a"); + values.emplace_back("b"); + values.emplace_back("c"); + ctx.set("value", values); + hbs.registerPartial("list", "{{#each .}}{{> @partial-block}}{{/each}}"); + BOOST_TEST( + hbs.render("", ctx) == + ""); + } + + // should render block from partial with context (twice) + { + dom::Object value; + value.set("value", "success"); + dom::Object ctx; + ctx.set("context", value); + hbs.registerPartial("dude", "{{#with context}}{{> @partial-block }} {{> @partial-block }}{{/with}}"); + BOOST_TEST(hbs.render("{{#> dude}}{{value}}{{/dude}}", ctx) == "success success"); + } + + // should render block from partial with context + { + 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"); + } + + // should render block from partial with block params + { + dom::Object value; + value.set("value", "success"); + dom::Object ctx; + ctx.set("context", value); + hbs.registerPartial("dude", "{{> @partial-block }}"); + BOOST_TEST(hbs.render("{{#with context as |me|}}{{#> dude}}{{me.value}}{{/dude}}{{/with}}", ctx) == "success"); + } + + // should render nested partial blocks + { + dom::Object value; + value.set("value", "success"); + hbs.registerPartial("outer", "{{#> nested}}{{> @partial-block}}{{/nested}}"); + hbs.registerPartial("nested", "{{> @partial-block}}"); + BOOST_TEST( + hbs.render("", value) == + ""); + } + + // should render nested partial blocks at different nesting levels + { + dom::Object value; + value.set("value", "success"); + hbs.registerPartial("outer", "{{#> nested}}{{> @partial-block}}{{/nested}}{{> @partial-block}}"); + hbs.registerPartial("nested", "{{> @partial-block}}"); + BOOST_TEST( + hbs.render("", value) == + ""); + } + + // should render nested partial blocks at different nesting levels (twice) + { + dom::Object value; + value.set("value", "success"); + hbs.registerPartial("outer", "{{#> nested}}{{> @partial-block}} {{> @partial-block}}{{/nested}}{{> @partial-block}}+{{> @partial-block}}"); + hbs.registerPartial("nested", "{{> @partial-block}}"); + BOOST_TEST( + hbs.render("", value) == + ""); + } + + // should render nested partial blocks (twice at each level) + { + dom::Object value; + value.set("value", "success"); + hbs.registerPartial("outer", "{{#> nested}}{{> @partial-block}} {{> @partial-block}}{{/nested}}"); + hbs.registerPartial("nested", "{{> @partial-block}}{{> @partial-block}}"); + BOOST_TEST( + hbs.render("", value) == + ""); + } +} + +void +inline_partials() +{ + Handlebars hbs; + + // should define inline partials for template + { + BOOST_TEST( + hbs.render("{{#*inline \"myPartial\"}}success{{/inline}}{{> myPartial}}") == + "success"); + } + + // should overwrite multiple partials in the same template + { + BOOST_TEST( + hbs.render("{{#*inline \"myPartial\"}}fail{{/inline}}{{#*inline \"myPartial\"}}success{{/inline}}{{> myPartial}}") == + "success"); + } + + // should define inline partials for block + { + BOOST_TEST( + hbs.render("{{#with .}}{{#*inline \"myPartial\"}}success{{/inline}}{{> myPartial}}{{/with}}") == + "success"); + + BOOST_TEST_THROW_WITH( + hbs.render("{{#with .}}{{#*inline \"myPartial\"}}success{{/inline}}{{/with}}{{> myPartial}}"), + HandlebarsError, "The partial myPartial could not be found"); + } + + // should override global partials + { + hbs.registerPartial("myPartial", "fail"); + BOOST_TEST( + hbs.render("{{#*inline \"myPartial\"}}success{{/inline}}{{> myPartial}}") == + "success"); + hbs.unregisterPartial("myPartial"); + } + + // should override template partials + { + BOOST_TEST( + hbs.render("{{#*inline \"myPartial\"}}fail{{/inline}}{{#with .}}{{#*inline \"myPartial\"}}success{{/inline}}{{> myPartial}}{{/with}}") == + "success"); + } + + // should override partials down the entire stack + { + BOOST_TEST( + hbs.render("{{#with .}}{{#*inline \"myPartial\"}}success{{/inline}}{{#with .}}{{#with .}}{{> myPartial}}{{/with}}{{/with}}{{/with}}") == + "success"); + } + + // should define inline partials for partial call + { + hbs.registerPartial("dude", "{{> myPartial }}"); + BOOST_TEST( + hbs.render("{{#*inline \"myPartial\"}}success{{/inline}}{{> dude}}") == + "success"); + hbs.unregisterPartial("dude"); + } + + // should define inline partials in partial block call + { + hbs.registerPartial("dude", "{{> myPartial }}"); + BOOST_TEST( + hbs.render("{{#> dude}}{{#*inline \"myPartial\"}}success{{/inline}}{{/dude}}") == + "success"); + hbs.unregisterPartial("dude"); + } + + // should render nested inline partials + { + dom::Object ctx; + ctx.set("value", "success"); + BOOST_TEST( + hbs.render( + "{{#*inline \"outer\"}}{{#>inner}}{{>@partial-block}}{{/inner}}{{/inline}}" + "{{#*inline \"inner\"}}{{>@partial-block}}{{/inline}}" + "{{#>outer}}{{value}}{{/outer}}", ctx) == + "success"); + } + + // should render nested inline partials with partial-blocks on different nesting levels + { + dom::Object ctx; + ctx.set("value", "success"); + BOOST_TEST( + hbs.render( + "{{#*inline \"outer\"}}{{#>inner}}{{>@partial-block}}{{/inner}}{{>@partial-block}}{{/inline}}" + "{{#*inline \"inner\"}}{{>@partial-block}}{{/inline}}" + "{{#>outer}}{{value}}{{/outer}}", ctx) == + "successsuccess"); + // {{#>outer}}{{value}}{{/outer}} + // {{#>inner}}{{value}}{{/inner}}{{value}} + // {{value}}{{/inner}}{{value}} + // success{{/inner}}success + } + + // should render nested inline partials (twice at each level) + { + dom::Object ctx; + ctx.set("value", "success"); + BOOST_TEST( + hbs.render( + "{{#*inline \"outer\"}}{{#>inner}}{{>@partial-block}} {{>@partial-block}}{{/inner}}{{/inline}}" + "{{#*inline \"inner\"}}{{>@partial-block}}{{>@partial-block}}{{/inline}}" + "{{#>outer}}{{value}}{{/outer}}", ctx) == + "success successsuccess success"); + } +} + +void +standalone_partials() +{ + Handlebars hbs; + + dom::Object hash; + dom::Array dudes; + dom::Object dude1; + dude1.set("name", "Yehuda"); + dude1.set("url", "http://yehuda"); + dudes.emplace_back(dude1); + dom::Object dude2; + dude2.set("name", "Alan"); + dude2.set("url", "http://alan"); + dudes.emplace_back(dude2); + hash.set("dudes", dudes); + + // indented partials + { + hbs.registerPartial("dude", "{{name}}\n"); + BOOST_TEST( + hbs.render("Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}", hash) == + "Dudes:\n Yehuda\n Alan\n"); + } + + // nested indented partials + { + hbs.registerPartial("dude", "{{name}}\n {{> url}}"); + hbs.registerPartial("url", "{{url}}!\n"); + BOOST_TEST( + hbs.render("Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}", hash) == + "Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n"); + } + + // prevent nested indented partials + { + hbs.registerPartial("dude", "{{name}}\n {{> url}}"); + hbs.registerPartial("url", "{{url}}!\n"); + HandlebarsOptions opt; + opt.preventIndent = true; + BOOST_TEST( + hbs.render("Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}", hash, opt) == + "Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n"); + } +} + +void +partial_compat_mode() +{ + Handlebars hbs; + + dom::Object root; + root.set("root", "yes"); + dom::Array dudes; + dom::Object dude1; + dude1.set("name", "Yehuda"); + dude1.set("url", "http://yehuda"); + dudes.emplace_back(dude1); + dom::Object dude2; + dude2.set("name", "Alan"); + dude2.set("url", "http://alan"); + dudes.emplace_back(dude2); + root.set("dudes", dudes); + + HandlebarsOptions compat; + compat.compat = true; + + // partials can access parents + { + hbs.registerPartial("dude", "{{name}} ({{url}}) {{root}} "); + BOOST_TEST( + hbs.render("Dudes: {{#dudes}}{{> dude}}{{/dudes}}", root, compat) == + "Dudes: Yehuda (http://yehuda) yes Alan (http://alan) yes "); + } + + // partials can access parents with custom context + { + hbs.registerPartial("dude", "{{name}} ({{url}}) {{root}} "); + BOOST_TEST( + hbs.render("Dudes: {{#dudes}}{{> dude \"test\"}}{{/dudes}}", root, compat) == + "Dudes: Yehuda (http://yehuda) yes Alan (http://alan) yes "); + } + + // partials can access parents without data + { + hbs.registerPartial("dude", "{{name}} ({{url}}) {{root}} "); + compat.data = false; + BOOST_TEST( + hbs.render("Dudes: {{#dudes}}{{> dude}}{{/dudes}}", root, compat) == + "Dudes: Yehuda (http://yehuda) yes Alan (http://alan) yes "); + compat.data = nullptr; + } + + // partials inherit compat + { + hbs.registerPartial("dude", "{{#dudes}}{{name}} ({{url}}) {{root}} {{/dudes}}"); + BOOST_TEST( + hbs.render("Dudes: {{> dude}}", root, compat) == + "Dudes: Yehuda (http://yehuda) yes Alan (http://alan) yes "); + } +} + void run() { @@ -1286,6 +1887,11 @@ run() safe_string(); basic_context(); whitespace_control(); + partials(); + partial_blocks(); + inline_partials(); + standalone_partials(); + partial_compat_mode(); } }; diff --git a/test-files/handlebars/features_test.adoc b/test-files/handlebars/features_test.adoc index 85abb2961..7af994d49 100644 --- a/test-files/handlebars/features_test.adoc +++ b/test-files/handlebars/features_test.adoc @@ -29,7 +29,6 @@ struct from_chars }; ---- - // #with to change context Person: John Doe in page about `from_chars` @@ -327,7 +326,6 @@ Missing: true() {{BAR}} - == Partials // Basic partials @@ -337,7 +335,6 @@ struct from_chars { }; ---- - [,cpp] ---- struct from_chars @@ -345,17 +342,12 @@ struct from_chars }; ---- - // Dynamic partials -Dynamo! -Found! - +Dynamo!Found! // Partial context switch Interesting! - // Partial parameters The result is 123 - Hello, Alice Doe. Hello, Bob Doe. Hello, Carol Smith. @@ -364,25 +356,17 @@ The result is 123 // Partial blocks Failover content - // Pass templates to partials Site Content My Content - // Inline partials - My Content - My Content - My Content - + My Content My Content My Content // Block inline partials + My Nav
- My Content -
- + My Content == Blocks // Block noop @@ -1438,6 +1422,5 @@ c // Inverse block with no helper expands expressions - struct T - + struct T