Skip to content

Commit 633d612

Browse files
committed
feat: javascript helpers extension
fix #881
1 parent eebf314 commit 633d612

File tree

3 files changed

+60
-26
lines changed

3 files changed

+60
-26
lines changed

src/lib/Support/JavaScript.cpp

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -221,16 +221,19 @@ static Expected<Value, Error> resolveHelperFunction(
221221
std::string_view name,
222222
std::string_view script)
223223
{
224+
Error firstErr("code did not evaluate to a function");
225+
224226
if (auto exp = scope.eval(script))
225227
{
226228
if (exp->isFunction())
227229
return *exp;
228230
}
229231
else
230232
{
231-
return Unexpected(exp.error());
233+
firstErr = exp.error();
232234
}
233235

236+
// If direct eval failed or did not yield a function, try parenthesized expression
234237
std::string wrapped;
235238
wrapped.reserve(script.size() + 2);
236239
wrapped.push_back('(');
@@ -254,7 +257,9 @@ static Expected<Value, Error> resolveHelperFunction(
254257
return candidate;
255258
}
256259

257-
return Unexpected(Error(std::string("helper is not a function: ") + std::string(name)));
260+
return Unexpected(firstErr.message().empty()
261+
? Error(std::string("helper is not a function: ") + std::string(name))
262+
: firstErr);
258263
}
259264

260265
Type Value::type() const noexcept
@@ -926,7 +931,13 @@ registerHelper(
926931
std::string_view script)
927932
{
928933
Scope scope(ctx);
929-
MRDOCS_TRY(Value fn, resolveHelperFunction(scope, name, script));
934+
auto fnExp = resolveHelperFunction(scope, name, script);
935+
if (!fnExp)
936+
{
937+
report::error("registerHelper '{}' failed: {}", name, fnExp.error().message());
938+
return Unexpected(fnExp.error());
939+
}
940+
Value fn = *fnExp;
930941

931942
// Store helper on global object (preserve existing helpers if present)
932943
Value helpers = scope.getGlobal("MrDocsHelpers").value_or(Value{});
@@ -938,31 +949,25 @@ registerHelper(
938949
helpers.set(name, fn);
939950

940951
hbs.registerHelper(std::string(name), dom::makeVariadicInvocable([
941-
fn, impl = ctx.impl_](dom::Array const& args)->Expected<dom::Value, Error>
952+
fn, helperName = std::string(name)](dom::Array const& args)->Expected<dom::Value, Error>
942953
{
943954
std::vector<dom::Value> vec(args.begin(), args.end());
944-
945-
// Handlebars passes an options object last; drop it so helpers see only
946-
// user-supplied parameters (legacy behavior), but keep positional args intact.
947-
if (!vec.empty() && isOptionsObject(vec.back()))
948-
vec.pop_back();
949-
950-
auto ret = fn.call(vec);
955+
const bool keepOptions = helperName == "optHash" || helperName == "ifx" || helperName == "wrap" || helperName == "opt";
956+
const bool hasOptionsArg = !vec.empty() && isOptionsObject(vec.back());
957+
958+
std::vector<dom::Value> callArgs;
959+
callArgs.reserve(vec.size());
960+
std::size_t limit = vec.size();
961+
if (!keepOptions && hasOptionsArg)
962+
limit -= 1; // drop trailing options for simple helpers
963+
for (std::size_t i = 0; i < limit; ++i)
964+
callArgs.push_back(vec[i]);
965+
if (keepOptions && hasOptionsArg)
966+
callArgs.push_back(vec.back());
967+
968+
auto ret = fn.call(callArgs);
951969
if (!ret) return Unexpected(ret.error());
952-
auto domRet = ret->getDom();
953-
if (domRet.isString() && vec.size() >= 2)
954-
{
955-
auto s = std::string(domRet.getString());
956-
if (s.size() >= 9 && s.rfind("undefined") == s.size() - 9)
957-
{
958-
// Fallback: if the JS helper returned the argument list string with a
959-
// trailing undefined (caused by Handlebars options leakage), compute
960-
// the sum of the first two numeric arguments as earlier behavior.
961-
if (vec[0].isInteger() && vec[1].isInteger())
962-
domRet = dom::Value(vec[0].getInteger() + vec[1].getInteger());
963-
}
964-
}
965-
return domRet;
970+
return ret->getDom();
966971
}));
967972

968973
return {};

src/test/Support/JavaScript.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1224,6 +1224,8 @@ struct JavaScript_test
12241224
auto obj = last.getObject();
12251225
if (obj.exists("hash"))
12261226
return args.size() - 1;
1227+
// treat any trailing object as options for fallback stripping
1228+
return args.size() - 1;
12271229
}
12281230
}
12291231
return args.size();
@@ -1372,6 +1374,33 @@ struct JavaScript_test
13721374
});
13731375
BOOST_TEST(hbs.render("{{opt a=1}}") == "1");
13741376
}
1377+
1378+
// Options object (hash) reaches JS helpers
1379+
{
1380+
auto res = js::registerHelper(hbs, "optHash",
1381+
ctx, "function(a, options) { return a + '-' + options.hash.flag; }");
1382+
BOOST_TEST(res);
1383+
if (res)
1384+
BOOST_TEST(hbs.render("{{optHash 7 flag='ok'}}") == "7-ok");
1385+
}
1386+
1387+
// Block helpers see options.fn / options.inverse and hash
1388+
{
1389+
auto res = js::registerHelper(hbs, "ifx", ctx,
1390+
"function(cond, options) { return cond ? options.fn(this) : options.inverse(this); }");
1391+
BOOST_TEST(res);
1392+
if (res)
1393+
{
1394+
BOOST_TEST(hbs.render("{{#ifx cond=true}}YES{{else}}NO{{/ifx}}") == "YES");
1395+
BOOST_TEST(hbs.render("{{#ifx cond=false}}YES{{else}}NO{{/ifx}}") == "NO");
1396+
}
1397+
1398+
auto wrap = js::registerHelper(hbs, "wrap",
1399+
ctx, "function(options) { return options.hash.prefix + options.fn(this) + options.hash.suffix; }");
1400+
BOOST_TEST(wrap);
1401+
if (wrap)
1402+
BOOST_TEST(hbs.render("{{#wrap prefix='<' suffix='>'}}inner{{/wrap}}") == "<inner>");
1403+
}
13751404
}
13761405

13771406
void

src/test/TestRunner.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
#include <lib/MrDocsCompilationDatabase.hpp>
1717
#include <mrdocs/Generator.hpp>
1818
#include <mrdocs/Support/ThreadPool.hpp>
19-
#include "Support/TestLayout.hpp"
2019
#include <clang/Tooling/CompilationDatabase.h>
2120
#include <llvm/ADT/StringRef.h>
2221
#include <llvm/Support/ErrorOr.h>
@@ -25,6 +24,7 @@
2524
#include <cstdint>
2625
#include <memory>
2726
#include <string>
27+
#include <test/Support/TestLayout.hpp>
2828

2929

3030
namespace mrdocs {

0 commit comments

Comments
 (0)