From d10a92142e7420d7af4656048e548ee314fd9ff9 Mon Sep 17 00:00:00 2001 From: alandefreitas Date: Tue, 5 Sep 2023 11:50:49 -0300 Subject: [PATCH] feat: util specs --- include/mrdox/Dom/Kind.hpp | 2 + include/mrdox/Dom/Value.hpp | 42 +++++++++------- include/mrdox/Support/Error.hpp | 4 +- include/mrdox/Support/Handlebars.hpp | 53 +++++++++++--------- share/gdb/mrdox_printers.py | 14 +++--- src/lib/Dom/Array.cpp | 8 +++ src/lib/Dom/Value.cpp | 74 ++++++++++++++++++++-------- src/lib/Support/Handlebars.cpp | 36 +++++++------- src/test/lib/Support/Handlebars.cpp | 72 +++++++++++++++++++++++++-- 9 files changed, 212 insertions(+), 93 deletions(-) diff --git a/include/mrdox/Dom/Kind.hpp b/include/mrdox/Dom/Kind.hpp index 0276146b0..5be86b4a4 100644 --- a/include/mrdox/Dom/Kind.hpp +++ b/include/mrdox/Dom/Kind.hpp @@ -21,10 +21,12 @@ namespace dom { */ enum class Kind { + Undefined, Null, Boolean, Integer, String, + SafeString, Array, Object, Function diff --git a/include/mrdox/Dom/Value.hpp b/include/mrdox/Dom/Value.hpp index ead3f2017..5d40fc288 100644 --- a/include/mrdox/Dom/Value.hpp +++ b/include/mrdox/Dom/Value.hpp @@ -24,6 +24,10 @@ namespace clang { namespace mrdox { + +dom::Value +safeString(std::string_view str); + namespace dom { /** A variant container for any kind of Dom value. @@ -31,17 +35,6 @@ namespace dom { class MRDOX_DECL Value { - enum class Kind - { - Null, - Boolean, - Integer, - String, - Array, - Object, - Function - }; - Kind kind_; union @@ -56,6 +49,7 @@ class MRDOX_DECL friend class Array; friend class Object; + friend Value clang::mrdox::safeString(std::string_view s); public: ~Value(); @@ -65,6 +59,7 @@ class MRDOX_DECL Value& operator=(Value const&); Value& operator=(Value&&) noexcept; + Value(dom::Kind kind) noexcept; Value(std::nullptr_t) noexcept; Value(std::int64_t) noexcept; Value(String str) noexcept; @@ -72,6 +67,7 @@ class MRDOX_DECL Value(Object obj) noexcept; Value(Function fn) noexcept; + template requires (!std::same_as) Value(T v) noexcept : Value(std::int64_t(v)) {} @@ -85,7 +81,7 @@ class MRDOX_DECL } template - requires std::is_enum_v + requires std::is_enum_v && (!std::same_as) Value(Enum v) noexcept : Value(static_cast< std::underlying_type_t>(v)) @@ -99,16 +95,12 @@ class MRDOX_DECL } // VFALCO Should this be a literal? -#if 0 Value(char const* s) : Value(String(s)) { } -#endif - template - requires std::convertible_to< - StringLike, String> + template StringLike> Value(StringLike const& s) : Value(String(s)) { @@ -136,6 +128,13 @@ class MRDOX_DECL */ dom::Kind kind() const noexcept; + /** Return true if this is undefined. + */ + bool isUndefined() const noexcept + { + return kind_ == Kind::Undefined; + } + /** Return true if this is null. */ bool isNull() const noexcept @@ -164,6 +163,13 @@ class MRDOX_DECL return kind_ == Kind::String; } + /** Return true if this is a safe string. + */ + bool isSafeString() const noexcept + { + return kind_ == Kind::SafeString; + } + /** Return true if this is an array. */ bool isArray() const noexcept @@ -203,7 +209,7 @@ class MRDOX_DECL String const& getString() const noexcept { - MRDOX_ASSERT(isString()); + MRDOX_ASSERT(isString() || isSafeString()); return str_; } diff --git a/include/mrdox/Support/Error.hpp b/include/mrdox/Support/Error.hpp index 27465ae6f..9d81fa367 100644 --- a/include/mrdox/Support/Error.hpp +++ b/include/mrdox/Support/Error.hpp @@ -524,8 +524,8 @@ Expected( U&& u) : has_error_(false) { - static_assert(std::is_nothrow_convertible_v); - static_assert(! std::convertible_to); + static_assert(std::is_convertible_v); + static_assert(!std::convertible_to); std::construct_at(&v_, std::forward(u)); } diff --git a/include/mrdox/Support/Handlebars.hpp b/include/mrdox/Support/Handlebars.hpp index 0e5d91e95..b52b33bc9 100644 --- a/include/mrdox/Support/Handlebars.hpp +++ b/include/mrdox/Support/Handlebars.hpp @@ -120,10 +120,6 @@ namespace detail { st.append( sv.data(), sv.data() + sv.size() ); }; - struct MRDOX_DECL safeStringWrapper { - std::string v_; - }; - struct RenderState; // Heterogeneous lookup support @@ -1037,8 +1033,7 @@ class Handlebars { using R = std::invoke_result_t; static_assert( std::same_as || - std::convertible_to || - std::same_as); + std::convertible_to); registerHelperImpl(name, helper_type([helper = std::forward(helper)]( dom::Array const& args, HandlebarsCallback const& options) -> std::pair { @@ -1051,10 +1046,6 @@ class Handlebars { helper(args, options); return {nullptr, HelperBehavior::NO_RENDER}; } - else if constexpr (std::same_as) - { - return {helper(args, options).v_, HelperBehavior::RENDER_RESULT_NOESCAPE}; - } })); } @@ -1084,8 +1075,7 @@ class Handlebars { using R = std::invoke_result_t; static_assert( std::same_as || - std::convertible_to || - std::same_as); + std::convertible_to); registerHelperImpl(name, helper_type([helper = std::forward(helper)]( dom::Array const& args, HandlebarsCallback const&) -> std::pair { @@ -1098,10 +1088,6 @@ class Handlebars { helper(args); return {nullptr, HelperBehavior::NO_RENDER}; } - else if constexpr (std::same_as) - { - return {helper(args).v_, HelperBehavior::RENDER_RESULT_NOESCAPE}; - } })); } @@ -1134,8 +1120,7 @@ class Handlebars { using R = std::invoke_result_t; static_assert( std::same_as || - std::convertible_to || - std::same_as); + std::convertible_to); registerHelperImpl(name, helper_type([helper = std::forward(helper)]( dom::Array const&, HandlebarsCallback const&) -> std::pair { @@ -1147,10 +1132,6 @@ class Handlebars { { helper(); return {nullptr, HelperBehavior::NO_RENDER}; - } - else if constexpr (std::same_as) - { - return {helper().v_, HelperBehavior::RENDER_RESULT_NOESCAPE}; } })); } @@ -1354,7 +1335,7 @@ createFrame(dom::Object const& parent); @see https://handlebarsjs.com/api-reference/utilities.html#handlebars-safestring-string */ MRDOX_DECL -detail::safeStringWrapper +dom::Value safeString(std::string_view str); /** HTML escapes the specified string @@ -1388,6 +1369,32 @@ escapeExpression( OutputRef out, std::string_view str); +template DomValue> +requires (!std::convertible_to) +std::string +escapeExpression( + DomValue const& val) +{ + dom::Value v = val; + if (v.isString()) + { + return escapeExpression(std::string_view(v.getString())); + } + if (v.isObject() && v.getObject().exists("toHTML")) + { + dom::Value fn = v.getObject().find("toHTML"); + if (fn.isFunction()) { + return toString(fn.getFunction()()); + } + } + if (v.isNull() || v.isUndefined()) + { + return {}; + } + return toString(v); +} + + /** An error thrown or returned by Handlebars An error returned or thrown by Handlebars environment when diff --git a/share/gdb/mrdox_printers.py b/share/gdb/mrdox_printers.py index f5b868905..24d371d6c 100644 --- a/share/gdb/mrdox_printers.py +++ b/share/gdb/mrdox_printers.py @@ -74,17 +74,19 @@ def __init__(self, value): def children(self): # Get kind enum kind = self.value['kind_'] - if kind == 1: + if kind == 2: yield 'Boolean', self.value['b_'] - elif kind == 2: - yield 'Integer', self.value['i_'] elif kind == 3: - yield 'String', self.value['str_'] + yield 'Integer', self.value['i_'] elif kind == 4: - yield 'Array', self.value['arr_'] + yield 'String', self.value['str_'] elif kind == 5: - yield 'Object', self.value['obj_']['impl_']['_M_ptr'] + yield 'SafeString', self.value['str_'] elif kind == 6: + yield 'Array', self.value['arr_'] + elif kind == 7: + yield 'Object', self.value['obj_']['impl_']['_M_ptr'] + elif kind == 8: yield 'Function', self.value['fn_'] else: yield 'kind_', self.value['kind_'] diff --git a/src/lib/Dom/Array.cpp b/src/lib/Dom/Array.cpp index 5b02a5ff6..9a13ecfc2 100644 --- a/src/lib/Dom/Array.cpp +++ b/src/lib/Dom/Array.cpp @@ -38,6 +38,14 @@ Array( { } +Array:: +Array(storage_type elements) + : impl_(std::make_shared()) +{ + for(auto const& e : elements) + impl_->emplace_back(e); +} + Array& Array:: operator=( diff --git a/src/lib/Dom/Value.cpp b/src/lib/Dom/Value.cpp index 8e3a8602a..5d88ef65f 100644 --- a/src/lib/Dom/Value.cpp +++ b/src/lib/Dom/Value.cpp @@ -20,11 +20,13 @@ Value:: { switch(kind_) { + case Kind::Undefined: case Kind::Null: case Kind::Boolean: case Kind::Integer: break; case Kind::String: + case Kind::SafeString: std::destroy_at(&str_); break; case Kind::Array: @@ -54,6 +56,7 @@ Value( { switch(kind_) { + case Kind::Undefined: case Kind::Null: break; case Kind::Boolean: @@ -63,6 +66,7 @@ Value( std::construct_at(&i_, other.i_); break; case Kind::String: + case Kind::SafeString: std::construct_at(&str_, other.str_); break; case Kind::Array: @@ -86,6 +90,7 @@ Value( { switch(kind_) { + case Kind::Undefined: case Kind::Null: break; case Kind::Boolean: @@ -95,6 +100,7 @@ Value( std::construct_at(&i_, other.i_); break; case Kind::String: + case Kind::SafeString: std::construct_at(&str_, std::move(other.str_)); std::destroy_at(&other.str_); break; @@ -138,6 +144,39 @@ operator=( //------------------------------------------------ +Value:: +Value(dom::Kind kind) noexcept + : kind_(kind) +{ + switch(kind_) + { + case Kind::Undefined: + case Kind::Null: + break; + case Kind::Boolean: + std::construct_at(&b_, false); + break; + case Kind::Integer: + std::construct_at(&i_, 0); + break; + case Kind::String: + case Kind::SafeString: + std::construct_at(&str_); + break; + case Kind::Array: + std::construct_at(&arr_); + break; + case Kind::Object: + std::construct_at(&obj_); + break; + case Kind::Function: + std::construct_at(&fn_); + break; + default: + MRDOX_UNREACHABLE(); + } +} + Value:: Value( std::nullptr_t) noexcept @@ -209,18 +248,7 @@ dom::Kind Value:: kind() const noexcept { - switch(kind_) - { - case Kind::Null: return dom::Kind::Null; - case Kind::Boolean: return dom::Kind::Boolean; - case Kind::Integer: return dom::Kind::Integer; - case Kind::String: return dom::Kind::String; - case Kind::Array: return dom::Kind::Array; - case Kind::Object: return dom::Kind::Object; - case Kind::Function: return dom::Kind::Function; - default: - MRDOX_UNREACHABLE(); - } + return kind_; } bool @@ -441,9 +469,9 @@ toString( { switch(value.kind_) { - case Value::Kind::Array: + case Kind::Array: return toString(value.arr_); - case Value::Kind::Object: + case Kind::Object: return toString(value.obj_); default: return toStringChild(value); @@ -456,23 +484,27 @@ toStringChild( { switch(value.kind_) { - case Value::Kind::Null: + case Kind::Undefined: + return "undefined"; + case Kind::Null: return "null"; - case Value::Kind::Boolean: + case Kind::Boolean: return value.b_ ? "true" : "false"; - case Value::Kind::Integer: + case Kind::Integer: return std::to_string(value.i_); - case Value::Kind::String: + case Kind::String: return fmt::format("{}", value.str_); - case Value::Kind::Array: + case Kind::SafeString: + return fmt::format("{}", value.str_); + case Kind::Array: { if(! value.arr_.empty()) return "[...]"; return "[]"; } - case Value::Kind::Object: + case Kind::Object: return "[object Object]"; - case Value::Kind::Function: + case Kind::Function: return "[function]"; default: MRDOX_UNREACHABLE(); diff --git a/src/lib/Support/Handlebars.cpp b/src/lib/Support/Handlebars.cpp index 1d6007c9f..ddf283617 100644 --- a/src/lib/Support/Handlebars.cpp +++ b/src/lib/Support/Handlebars.cpp @@ -76,12 +76,14 @@ isTruthy(dom::Value const& arg) case dom::Kind::Integer: return arg.getInteger() != 0; case dom::Kind::String: + case dom::Kind::SafeString: return !arg.getString().empty(); case dom::Kind::Array: case dom::Kind::Object: case dom::Kind::Function: return true; case dom::Kind::Null: + case dom::Kind::Undefined: return false; default: MRDOX_UNREACHABLE(); @@ -174,11 +176,11 @@ createFrame(dom::Object const& child, dom::Object const& parent) return dom::newObject(child, parent); } -detail::safeStringWrapper +dom::Value safeString(std::string_view str) { - detail::safeStringWrapper w; - w.v_ = str; + dom::Value w(str); + w.kind_ = dom::Kind::SafeString; return w; } @@ -225,6 +227,9 @@ escapeExpression( case '`': out << "`"; break; + case '=': + out << "="; + break; default: out << c; break; @@ -250,6 +255,8 @@ format_to( { if (value.isString()) { escapeExpression(out, value.getString(), opt); + } else if (value.isSafeString()) { + out << value.getString(); } else if (value.isInteger()) { out << value.getInteger(); } else if (value.isBoolean()) { @@ -2157,9 +2164,7 @@ renderExpression( setupArgs(tag.arguments, context, state, args, cb, noStrict); auto [res, render] = fn(args, cb); if (render == HelperBehavior::RENDER_RESULT) { - format_to(out, res, opt2); - } else if (render == HelperBehavior::RENDER_RESULT_NOESCAPE) { - opt2.noEscape = true; + opt2.noEscape = opt2.noEscape || res.isSafeString(); format_to(out, res, opt2); } if (tag.removeRWhitespace) { @@ -2214,9 +2219,7 @@ renderExpression( setupArgs(tag.arguments, context, state, args, cb, opt); auto [res, render] = fn(args, cb); if (render == HelperBehavior::RENDER_RESULT) { - format_to(out, res, opt2); - } else if (render == HelperBehavior::RENDER_RESULT_NOESCAPE) { - opt2.noEscape = true; + opt2.noEscape = opt2.noEscape || res.isSafeString(); format_to(out, res, opt2); } if (tag.removeRWhitespace) { @@ -2820,20 +2823,11 @@ renderBlock( state.dataStack.emplace_back(state.data); auto [res, render] = fn(args, cb); if (render != HelperBehavior::NO_RENDER) { - // Always unescaped - HandlebarsOptions opt2 = opt; - opt2.noEscape = true; - format_to(out, res, opt2); - } -#if 0 - if (render == HelperBehavior::RENDER_RESULT) { - format_to(out, res, opt); - } else if (render == HelperBehavior::RENDER_RESULT_NOESCAPE) { + // Block helpers are always unescaped HandlebarsOptions opt2 = opt; opt2.noEscape = true; format_to(out, res, opt2); } -#endif state.inlinePartials.pop_back(); // state.parentContext.pop_back(); state.dataStack.pop_back(); @@ -2972,6 +2966,7 @@ isSame(dom::Value const& a, dom::Value const& b) { return false; } switch (a.kind()) { + case dom::Kind::Undefined: case dom::Kind::Null: return true; case dom::Kind::Boolean: @@ -2979,6 +2974,7 @@ isSame(dom::Value const& a, dom::Value const& b) { case dom::Kind::Integer: return a.getInteger() == b.getInteger(); case dom::Kind::String: + case dom::Kind::SafeString: return a.getString() == b.getString(); case dom::Kind::Array: return isSame(a.getArray(), b.getArray()); @@ -3049,6 +3045,7 @@ isLt(dom::Value const& a, dom::Value const& b) { return false; } switch (a.kind()) { + case dom::Kind::Undefined: case dom::Kind::Null: return false; case dom::Kind::Boolean: @@ -3056,6 +3053,7 @@ isLt(dom::Value const& a, dom::Value const& b) { case dom::Kind::Integer: return a.getInteger() < b.getInteger(); case dom::Kind::String: + case dom::Kind::SafeString: return a.getString().get() < b.getString().get(); case dom::Kind::Array: return isLt(a.getArray(), b.getArray()); diff --git a/src/test/lib/Support/Handlebars.cpp b/src/test/lib/Support/Handlebars.cpp index 80ea501e6..a85f9bd6f 100644 --- a/src/test/lib/Support/Handlebars.cpp +++ b/src/test/lib/Support/Handlebars.cpp @@ -26,6 +26,8 @@ to_string(clang::mrdox::dom::Kind const& value) { switch (value) { + case clang::mrdox::dom::Kind::Undefined: + return "undefined"; case clang::mrdox::dom::Kind::Null: return "null"; case clang::mrdox::dom::Kind::Boolean: @@ -34,6 +36,8 @@ to_string(clang::mrdox::dom::Kind const& value) return "integer"; case clang::mrdox::dom::Kind::String: return "string"; + case clang::mrdox::dom::Kind::SafeString: + return "safeString"; case clang::mrdox::dom::Kind::Array: return "array"; case clang::mrdox::dom::Kind::Object: @@ -3888,12 +3892,13 @@ helpers() return dom::Value("winning"); }); hash.set("helper", helper); - hash.set("hash", hash); + // hash.set("hash", hash); + dom::Object hash2; + hash2.set("helper", helper); + hash.set("hash", hash2); hbs.registerHelper("./helper", []() { return "fail"; }); - // BOOST_TEST(hbs.render("{{./helper 1}}", hash) == "winning"); - // AFREITAS: Investigate rules to pick from context or helpers BOOST_TEST(hbs.render("{{./helper 1}}", hash) == "fail"); BOOST_TEST(hbs.render("{{hash/helper 1}}", hash) == "winning"); } @@ -4975,7 +4980,6 @@ void strict() { // https://github.com/handlebars-lang/handlebars.js/blob/4.x/spec/strict.js - Handlebars hbs; HandlebarsOptions opt; @@ -5152,6 +5156,66 @@ void utils() { // https://github.com/handlebars-lang/handlebars.js/blob/4.x/spec/utils.js + Handlebars hbs; + + // SafeString + { + // it should not escape SafeString properties + { + dom::Value name = safeString("Sean O'Malley"); + dom::Object ctx; + ctx.set("name", name); + BOOST_TEST(hbs.render("{{name}}", ctx) == "Sean O'Malley"); + } + } + + // escapeExpression + { + // should escape html + { + BOOST_TEST(escapeExpression("foo<&\"'>") == "foo<&"'>"); + BOOST_TEST(escapeExpression("foo=") == "foo="); + } + + // should not escape SafeString + { + dom::Value string = safeString("foo<&\"'>"); + BOOST_TEST(escapeExpression(string) == "foo<&\"'>"); + + dom::Object ctx; + ctx.set("toHTML", dom::makeInvocable([]() { + return "foo<&\"'>"; + })); + BOOST_TEST(escapeExpression(ctx) == "foo<&\"'>"); + } + + // should handle falsy + { + BOOST_TEST(escapeExpression("").empty()); + BOOST_TEST(escapeExpression(dom::Value(dom::Kind::Undefined)).empty()); + BOOST_TEST(escapeExpression(dom::Value(dom::Kind::Null)).empty()); + BOOST_TEST(escapeExpression(false) == "false"); + BOOST_TEST(escapeExpression(0) == "0"); + } + } + + // isEmpty + { + // should be empty + BOOST_TEST(isEmpty(dom::Value(dom::Kind::Undefined))); + BOOST_TEST(isEmpty(dom::Value(dom::Kind::Null))); + BOOST_TEST(isEmpty(false)); + BOOST_TEST(isEmpty("")); + BOOST_TEST(isEmpty(dom::Array())); + + // should not be empty + BOOST_TEST(!isEmpty(0)); + BOOST_TEST(!isEmpty(dom::Array({1}))); + BOOST_TEST(!isEmpty("foo")); + dom::Object ctx; + ctx.set("bar", 1); + BOOST_TEST(!isEmpty(ctx)); + } } void