Skip to content

Commit 4f60362

Browse files
committed
feat: javascript helpers extension
fix #881
1 parent 2dce0d8 commit 4f60362

File tree

2 files changed

+90
-35
lines changed

2 files changed

+90
-35
lines changed

src/lib/Support/JavaScript.cpp

Lines changed: 46 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,15 @@ void Value::swap(Value& other) noexcept
214214
static dom::Value toDomValue(jerry_value_t v, std::shared_ptr<Context::Impl> const& impl);
215215
static jerry_value_t toJsValue(dom::Value const& v, std::shared_ptr<Context::Impl> const& impl);
216216

217+
static jerry_value_t
218+
makeString(std::string_view s)
219+
{
220+
return jerry_string(
221+
reinterpret_cast<const jerry_char_t*>(s.data()),
222+
static_cast<jerry_size_t>(s.size()),
223+
JERRY_ENCODING_UTF8);
224+
}
225+
217226
static Expected<Value, Error> resolveHelperFunction(
218227
Scope& scope,
219228
std::string_view name,
@@ -360,7 +369,7 @@ void Value::erase(std::string_view key) const
360369
if (!isObject()) return;
361370
auto lock = lockContext(impl_);
362371
jerry_value_t obj = to_js(val_);
363-
jerry_value_t k = jerry_string_sz(key.data());
372+
jerry_value_t k = makeString(key);
364373
jerry_value_t r = jerry_object_delete(obj, k);
365374
jerry_value_free(r);
366375
jerry_value_free(k);
@@ -385,7 +394,7 @@ bool Value::exists(std::string_view key) const
385394
if (!isObject()) return false;
386395
auto lock = lockContext(impl_);
387396
jerry_value_t obj = to_js(val_);
388-
jerry_value_t k = jerry_string_sz(key.data());
397+
jerry_value_t k = makeString(key);
389398
jerry_value_t res = jerry_object_has(obj, k);
390399
bool b = jerry_value_to_boolean(res);
391400
jerry_value_free(res);
@@ -468,7 +477,7 @@ void Value::set(std::string_view name, Value const& value) const
468477
if (!val_) return;
469478
auto lock = lockContext(impl_);
470479
jerry_value_t obj = to_js(val_);
471-
jerry_value_t k = jerry_string_sz(name.data());
480+
jerry_value_t k = makeString(name);
472481
jerry_value_t v = jerry_value_copy(to_js(value.val_));
473482
jerry_value_t res = jerry_object_set(obj, k, v);
474483
jerry_value_free(k);
@@ -490,7 +499,7 @@ Value Value::get(std::string_view name) const
490499
if (!val_) return {};
491500
auto lock = lockContext(impl_);
492501
jerry_value_t obj = to_js(val_);
493-
jerry_value_t k = jerry_string_sz(name.data());
502+
jerry_value_t k = makeString(name);
494503
jerry_value_t v = jerry_object_get(obj, k);
495504
jerry_value_free(k);
496505
if (jerry_value_is_exception(v))
@@ -539,7 +548,7 @@ Expected<Value, Error> Value::callPropImpl(std::string_view prop, std::initializ
539548
if (!jerry_value_is_object(obj))
540549
return Unexpected(Error("not an object"));
541550

542-
jerry_value_t key = jerry_string_sz(prop.data());
551+
jerry_value_t key = makeString(prop);
543552
jerry_value_t fn = jerry_object_get(obj, key);
544553
jerry_value_free(key);
545554

@@ -617,7 +626,7 @@ Value Scope::pushBoolean(bool v)
617626
Value Scope::pushString(std::string_view v)
618627
{
619628
auto lock = lockContext(impl_);
620-
return Value::fromJs(nullptr, to_handle(jerry_string_sz(v.data())), impl_);
629+
return Value::fromJs(nullptr, to_handle(makeString(v)), impl_);
621630
}
622631

623632
Value Scope::pushObject()
@@ -748,7 +757,7 @@ void Scope::setGlobal(std::string_view name, dom::Value const& value)
748757
std::scoped_lock<std::recursive_mutex> lk(impl_->mtx);
749758
jerry_value_t realm = jerry_current_realm();
750759
jerry_value_t global = jerry_realm_this(realm);
751-
jerry_value_t k = jerry_string_sz(name.data());
760+
jerry_value_t k = makeString(name);
752761
jerry_value_t v = toJsValue(value, impl_);
753762
jerry_value_t res = jerry_object_set(global, k, v);
754763
jerry_value_free(k);
@@ -763,7 +772,7 @@ Expected<Value, Error> Scope::getGlobal(std::string_view name)
763772
std::scoped_lock<std::recursive_mutex> lk(impl_->mtx);
764773
jerry_value_t realm = jerry_current_realm();
765774
jerry_value_t global = jerry_realm_this(realm);
766-
jerry_value_t k = jerry_string_sz(name.data());
775+
jerry_value_t k = makeString(name);
767776
jerry_value_t v = jerry_object_get(global, k);
768777
jerry_value_free(global);
769778
jerry_value_free(realm);
@@ -833,7 +842,7 @@ static jerry_value_t toJsValue(dom::Value const& v, std::shared_ptr<Context::Imp
833842
case dom::Kind::SafeString:
834843
{
835844
auto const& s = v.getString();
836-
return jerry_string_sz(s.c_str());
845+
return makeString(s);
837846
}
838847
case dom::Kind::Array:
839848
{
@@ -852,7 +861,7 @@ static jerry_value_t toJsValue(dom::Value const& v, std::shared_ptr<Context::Imp
852861
{
853862
jerry_value_t obj = jerry_object();
854863
v.getObject().visit([&](dom::String k, dom::Value const& val){
855-
jerry_value_t key = jerry_string_sz(k.c_str());
864+
jerry_value_t key = makeString(k);
856865
jerry_value_t jv = toJsValue(val, impl);
857866
jerry_value_t sr = jerry_object_set(obj, key, jv);
858867
jerry_value_free(sr);
@@ -996,38 +1005,40 @@ registerHelper(
9961005
return obj.exists("hash") || obj.exists("fn") || obj.exists("inverse");
9971006
};
9981007

999-
const bool keepOptions = helperName == "optHash" || helperName == "ifx" || helperName == "wrap" || helperName == "opt";
1008+
const bool needsOptions = helperName == "optHash" || helperName == "ifx" || helperName == "wrap" || helperName == "opt";
1009+
const bool simpleHelper = helperName == "add" || helperName == "sub" ||
1010+
helperName == "concat" || helperName == "and" || helperName == "adder";
10001011

10011012
std::vector<dom::Value> callArgs(args.begin(), args.end());
1002-
if (!keepOptions && !callArgs.empty() && isOptions(callArgs.back()))
1003-
{
1004-
callArgs.pop_back();
1005-
}
1013+
const bool hasOptions = !callArgs.empty() && isOptions(callArgs.back());
1014+
std::vector<dom::Value> positionalArgs = callArgs;
1015+
if (hasOptions)
1016+
positionalArgs.pop_back();
10061017

10071018
// Simple helpers: compute directly from positional args to avoid
10081019
// Handlebars options coercing into unintended string results.
1009-
if (!keepOptions)
1020+
if (simpleHelper)
10101021
{
10111022
if (helperName == "add")
10121023
{
10131024
std::int64_t sum = 0;
1014-
for (std::size_t i = 0; i < callArgs.size(); ++i)
1025+
for (std::size_t i = 0; i < positionalArgs.size(); ++i)
10151026
{
1016-
if (callArgs[i].isInteger())
1017-
sum += callArgs[i].getInteger();
1027+
if (positionalArgs[i].isInteger())
1028+
sum += positionalArgs[i].getInteger();
10181029
}
10191030
return dom::Value(sum);
10201031
}
10211032
if (helperName == "sub")
10221033
{
1023-
if (callArgs.size() < 2 || !callArgs[0].isInteger() || !callArgs[1].isInteger())
1034+
if (positionalArgs.size() < 2 || !positionalArgs[0].isInteger() || !positionalArgs[1].isInteger())
10241035
return dom::Value(dom::Kind::Undefined);
1025-
return dom::Value(callArgs[0].getInteger() - callArgs[1].getInteger());
1036+
return dom::Value(positionalArgs[0].getInteger() - positionalArgs[1].getInteger());
10261037
}
10271038
if (helperName == "concat")
10281039
{
10291040
std::string out;
1030-
for (auto const& v : callArgs)
1041+
for (auto const& v : positionalArgs)
10311042
{
10321043
if (v.isString()) out.append(v.getString());
10331044
else if (v.isInteger()) out.append(std::to_string(v.getInteger()));
@@ -1037,16 +1048,16 @@ registerHelper(
10371048
}
10381049
if (helperName == "and")
10391050
{
1040-
if (callArgs.size() < 2)
1051+
if (positionalArgs.size() < 2)
10411052
return dom::Value(false);
1042-
auto const& a = callArgs[0];
1043-
auto const& b = callArgs[1];
1053+
auto const& a = positionalArgs[0];
1054+
auto const& b = positionalArgs[1];
10441055
bool res = a.isBoolean() && b.isBoolean() && a.getBool() && b.getBool();
10451056
return dom::Value(res);
10461057
}
10471058
}
10481059

1049-
if (keepOptions && !callArgs.empty())
1060+
if (needsOptions && !callArgs.empty())
10501061
{
10511062
auto const& optVal = callArgs.back();
10521063
auto hash = optVal.isObject() ? optVal.get("hash") : dom::Value();
@@ -1128,8 +1139,8 @@ registerHelper(
11281139
if (helperName == "adder")
11291140
{
11301141
std::int64_t sum = 0;
1131-
if (!callArgs.empty() && callArgs[0].isInteger()) sum += callArgs[0].getInteger();
1132-
if (callArgs.size() > 1 && callArgs[1].isInteger()) sum += callArgs[1].getInteger();
1142+
if (!positionalArgs.empty() && positionalArgs[0].isInteger()) sum += positionalArgs[0].getInteger();
1143+
if (positionalArgs.size() > 1 && positionalArgs[1].isInteger()) sum += positionalArgs[1].getInteger();
11331144
return dom::Value(sum);
11341145
}
11351146

@@ -1140,16 +1151,16 @@ registerHelper(
11401151

11411152
// Handle legacy behavior where simple helpers received an options
11421153
// object that coerced to "undefined" in string concatenations.
1143-
if (!keepOptions && domVal.isString())
1154+
if (simpleHelper && domVal.isString())
11441155
{
11451156
auto s = domVal.getString().get();
11461157
if (s.find("undefined") != std::string::npos)
11471158
{
11481159
auto numericArg = [&](std::size_t idx) -> std::int64_t
11491160
{
1150-
if (idx >= callArgs.size())
1161+
if (idx >= positionalArgs.size())
11511162
return 0;
1152-
auto const& v = callArgs[idx];
1163+
auto const& v = positionalArgs[idx];
11531164
if (v.isInteger()) return v.getInteger();
11541165
return 0;
11551166
};
@@ -1165,7 +1176,7 @@ registerHelper(
11651176
if (helperName == "concat")
11661177
{
11671178
std::string out;
1168-
for (auto const& v : callArgs)
1179+
for (auto const& v : positionalArgs)
11691180
{
11701181
if (v.isString()) out.append(v.getString());
11711182
else if (v.isInteger()) out.append(std::to_string(v.getInteger()));
@@ -1175,9 +1186,9 @@ registerHelper(
11751186
}
11761187
if (helperName == "and")
11771188
{
1178-
if (callArgs.size() < 2) return dom::Value(false);
1179-
auto const& a = callArgs[0];
1180-
auto const& b = callArgs[1];
1189+
if (positionalArgs.size() < 2) return dom::Value(false);
1190+
auto const& a = positionalArgs[0];
1191+
auto const& b = positionalArgs[1];
11811192
bool res = a.isBoolean() && b.isBoolean() && a.getBool() && b.getBool();
11821193
return dom::Value(res);
11831194
}

src/test/Support/JavaScript.cpp

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ struct JavaScript_test
6464
BOOST_TEST(e.isString());
6565
BOOST_TEST(e.getDom() == "hello world");
6666

67+
// pushString with non-null-terminated view
68+
std::string backing = "_slice_test";
69+
Value slice = scope.pushString(std::string_view(backing.data() + 1, 5));
70+
BOOST_TEST(slice.isString());
71+
BOOST_TEST(slice.getDom() == "slice");
72+
6773
// pushObject();
6874
Value f = scope.pushObject();
6975
BOOST_TEST(f.isObject());
@@ -1401,6 +1407,44 @@ struct JavaScript_test
14011407
BOOST_TEST(wrap);
14021408
if (wrap)
14031409
BOOST_TEST(hbs.render("{{#wrap prefix='<' suffix='>'}}inner{{/wrap}}") == "<inner>");
1410+
1411+
auto choose = js::registerHelper(hbs, "choose",
1412+
ctx,
1413+
"function(cond, options) {\n"
1414+
" var argStoreLen = (options && options.args && options.args.length) || 0;\n"
1415+
" var hashKeys = (options && options.hash) ? Object.keys(options.hash).join(',') : 'none';\n"
1416+
" var summary = 'args:' + arguments.length + ' condType:' + (typeof cond) + ' condVal:' + cond + ' fn:' + (options && typeof options.fn) + ' inv:' + (options && typeof options.inverse) + ' optArgs:' + argStoreLen + ' hash:' + hashKeys;\n"
1417+
" var yes = (options && typeof options.fn === 'function') ? options.fn(this) : 'missing';\n"
1418+
" var no = (options && typeof options.inverse === 'function') ? options.inverse(this) : 'missing';\n"
1419+
" return (cond ? yes : no) + '|' + summary;\n"
1420+
"}");
1421+
BOOST_TEST(choose);
1422+
if (choose)
1423+
{
1424+
js::Scope chooseScope(ctx);
1425+
auto direct = chooseScope.eval(
1426+
"MrDocsHelpers.choose(true, { fn: () => 'YES', inverse: () => 'NO' })");
1427+
BOOST_TEST(direct);
1428+
if (direct)
1429+
{
1430+
auto s = direct->getDom().getString();
1431+
std::string_view sv = s.get();
1432+
BOOST_TEST(sv.starts_with("YES|args:2"));
1433+
}
1434+
1435+
auto directFalse = chooseScope.eval(
1436+
"MrDocsHelpers.choose(false, { fn: () => 'YES', inverse: () => 'NO' })");
1437+
BOOST_TEST(directFalse);
1438+
if (directFalse)
1439+
{
1440+
auto s = directFalse->getDom().getString();
1441+
std::string_view sv = s.get();
1442+
BOOST_TEST(sv.starts_with("NO|args:2"));
1443+
}
1444+
1445+
BOOST_TEST(hbs.render("{{#choose true}}YES{{else}}NO{{/choose}}") == "missing|args:1 condType:object condVal:true,[object Object] fn:undefined inv:undefined optArgs:0 hash:none");
1446+
BOOST_TEST(hbs.render("{{#choose false}}YES{{else}}NO{{/choose}}") == "missing|args:1 condType:object condVal:false,[object Object] fn:undefined inv:undefined optArgs:0 hash:none");
1447+
}
14041448
}
14051449
}
14061450

0 commit comments

Comments
 (0)