Skip to content

Commit 59227dc

Browse files
committed
feat: javascript helpers extension
fix #881
1 parent 03d9235 commit 59227dc

File tree

27 files changed

+197
-344
lines changed

27 files changed

+197
-344
lines changed

include/mrdocs/Support/JavaScript.hpp

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,17 +1063,32 @@ isFunction() const noexcept
10631063
as a helper function that can be called from
10641064
Handlebars templates.
10651065
1066-
The helper source is resolved via `resolveHelperFunction`
1067-
(direct eval → parenthesized eval → global lookup),
1068-
stored on the shared `MrDocsHelpers` object, and invoked with the Handlebars
1069-
options object supplied as the final argument.
1066+
The helper source is resolved in the following order:
1067+
1068+
1. **Parenthesized eval** - wraps the script in parentheses and evaluates.
1069+
Handles function declarations without side effects.
1070+
Example: `"function add(a, b) { return a + b; }"`
1071+
1072+
2. **Direct eval** - evaluates the script as-is.
1073+
Handles IIFEs and expressions that return functions.
1074+
Example: `"(function(){ return function(x){ return x*2; }; })()"`
1075+
1076+
3. **Global lookup** - looks up the helper name on the global object.
1077+
Handles scripts that define globals before returning.
1078+
Example: `"var helper = function(x){ return x; }; helper;"`
1079+
1080+
The resolved function is stored on the shared `MrDocsHelpers` global object
1081+
and registered with Handlebars. When invoked, positional arguments are passed
1082+
to the JavaScript function (the Handlebars options object is stripped to avoid
1083+
expensive recursive conversion of symbol contexts).
10701084
10711085
@param hbs The Handlebars instance to register the helper into
10721086
@param name The name of the helper function
10731087
@param ctx The JavaScript context to use
10741088
@param script The JavaScript code that defines the helper function
1089+
@return Success, or an error if the script could not be resolved to a function
10751090
*/
1076-
MRDOCS_DECL
1091+
[[nodiscard]] MRDOCS_DECL
10771092
Expected<void, Error>
10781093
registerHelper(
10791094
mrdocs::Handlebars& hbs,

src/lib/Gen/hbs/Builder.cpp

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,19 @@ std::vector<std::string>
5353
makeHelperDirs(std::vector<std::string> const& roots, std::string_view ext)
5454
{
5555
std::vector<std::string> dirs;
56-
dirs.reserve(roots.size());
56+
dirs.reserve(roots.size() * 2);
57+
58+
// Preserve root precedence: for each root load common first, then format.
59+
// Later roots (supplemental addons) still override earlier ones.
5760
for (auto const& root : roots)
5861
{
59-
auto const dir = files::appendPath(root, "generator", ext, "helpers");
60-
if (files::exists(dir))
61-
dirs.push_back(dir);
62+
auto const commonDir = files::appendPath(root, "generator", "common", "helpers");
63+
if (files::exists(commonDir))
64+
dirs.push_back(commonDir);
65+
66+
auto const formatDir = files::appendPath(root, "generator", ext, "helpers");
67+
if (files::exists(formatDir))
68+
dirs.push_back(formatDir);
6269
}
6370
return dirs;
6471
}

src/lib/Support/JavaScript.cpp

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,10 @@ invokeHelper(Value const& fn, dom::Array const& args)
7272
// Passing the options object would trigger expensive recursive conversion
7373
// of symbol contexts which contain circular references.
7474
std::vector<dom::Value> callArgs;
75-
callArgs.reserve(args.size());
76-
for (std::size_t i = 0; i + 1 < args.size(); ++i)
75+
callArgs.reserve(args.size() - 1);
76+
for (auto it = args.begin(); it != std::prev(args.end()); ++it)
7777
{
78-
callArgs.push_back(args.at(i));
78+
callArgs.push_back(*it);
7979
}
8080

8181
auto ret = fn.apply(callArgs);
@@ -1270,6 +1270,15 @@ makeObjectProxy(dom::Object obj, std::shared_ptr<Context::Impl> impl)
12701270
jerry_value_free(target);
12711271
jerry_value_free(handler);
12721272

1273+
// If proxy creation fails, return an empty object rather than an exception.
1274+
// The holder is cleaned up via the free_cb when holderRef (attached to
1275+
// handler) is garbage collected.
1276+
if (jerry_value_is_exception(proxy))
1277+
{
1278+
jerry_value_free(proxy);
1279+
return jerry_object();
1280+
}
1281+
12731282
return proxy;
12741283
}
12751284

@@ -1537,26 +1546,22 @@ resolveHelperFunction(
15371546
std::string_view name,
15381547
std::string_view script)
15391548
{
1540-
// Attempt to coerce user-provided helper source into a callable: first
1541-
// evaluate directly, then as a parenthesized expression, then fall back to
1542-
// a global lookup. This mirrors Handlebars' permissive helper loading while
1543-
// still rejecting non-functions with clear errors. Note: first two paths
1544-
// can execute side effects twice if the first expression is not a
1545-
// function; callers rely on deterministic ordering.
1549+
// Coerce user-provided helper source into a callable. Resolution order:
1550+
//
1551+
// 1. Parenthesized eval - handles function declarations without side effects
1552+
// e.g., "function add(a,b) { return a+b; }" -> "(function add(a,b)...)"
1553+
//
1554+
// 2. Direct eval - handles IIFEs and expressions that return functions
1555+
// e.g., "(function(){ return function(){}; })()"
1556+
//
1557+
// 3. Global lookup - handles scripts that define globals
1558+
// e.g., "var helper = function(){}; helper;"
1559+
//
1560+
// This order minimizes side effects: parenthesized eval of a function
1561+
// declaration is pure, while direct eval may execute statements.
15461562
Error firstErr("code did not evaluate to a function");
15471563

1548-
if (auto exp = scope.eval(script))
1549-
{
1550-
if (exp->isFunction())
1551-
{
1552-
return *exp;
1553-
}
1554-
}
1555-
else
1556-
{
1557-
firstErr = exp.error();
1558-
}
1559-
1564+
// Try parenthesized first (common case: function declarations)
15601565
std::string wrapped;
15611566
wrapped.reserve(script.size() + 2);
15621567
wrapped.push_back('(');
@@ -1572,9 +1577,24 @@ resolveHelperFunction(
15721577
}
15731578
else
15741579
{
1575-
return Unexpected(expr.error());
1580+
firstErr = expr.error();
1581+
}
1582+
1583+
// Try direct eval (IIFEs, expressions)
1584+
if (auto exp = scope.eval(script))
1585+
{
1586+
if (exp->isFunction())
1587+
{
1588+
return *exp;
1589+
}
1590+
}
1591+
else if (firstErr.message() == "code did not evaluate to a function")
1592+
{
1593+
// Keep the more informative error
1594+
firstErr = exp.error();
15761595
}
15771596

1597+
// Fall back to global lookup
15781598
if (Value global = scope.getGlobalObject())
15791599
{
15801600
Value candidate = global.get(name);

0 commit comments

Comments
 (0)