Skip to content

Commit aaf3aa5

Browse files
committed
feat: javascript helpers extension
fix #881
1 parent a97c487 commit aaf3aa5

File tree

5 files changed

+101
-68
lines changed

5 files changed

+101
-68
lines changed

src/lib/Gen/hbs/Builder.cpp

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,20 @@ templateDirs(Config const& config, std::string_view generator, std::string_view
5757

5858
namespace paths {
5959

60-
std::string selectExistingTemplateDir(std::vector<std::string> const& candidates)
60+
std::string selectExistingTemplateDir(
61+
std::vector<std::string> const& candidates,
62+
Preference pref)
6163
{
64+
std::string selected;
6265
for (auto const& dir : candidates)
6366
{
64-
if (files::exists(dir))
65-
return dir;
67+
if (!files::exists(dir))
68+
continue;
69+
selected = dir;
70+
if (pref == Preference::FirstExisting)
71+
break;
6672
}
67-
return {};
73+
return selected;
6874
}
6975

7076
std::string formatCandidateDirs(std::vector<std::string> const& candidates)
@@ -89,10 +95,11 @@ static std::string pickTemplateDir(
8995
Config const& config,
9096
std::string_view generator,
9197
std::string_view subdir,
92-
std::string_view purpose)
98+
std::string_view purpose,
99+
paths::Preference pref)
93100
{
94101
auto dirs = templateDirs(config, generator, subdir);
95-
if (auto const selected = paths::selectExistingTemplateDir(dirs); !selected.empty())
102+
if (auto const selected = paths::selectExistingTemplateDir(dirs, pref); !selected.empty())
96103
return selected;
97104

98105
auto const searched = paths::formatCandidateDirs(dirs);
@@ -254,8 +261,7 @@ Builder(
254261
loadPartials(hbs_, dir);
255262

256263
// Load JavaScript helpers
257-
auto const helperDirs = templateDirs(config, domCorpus.fileExtension, "helpers");
258-
for (auto const& dir : helperDirs)
264+
for (auto const& dir : templateDirs(config, domCorpus.fileExtension, "helpers"))
259265
{
260266
if (!files::exists(dir))
261267
continue;
@@ -330,32 +336,31 @@ Builder(
330336
hbs_.registerHelper("relativize", dom::makeInvocable(relativize_fn));
331337

332338
// Load layout templates
333-
std::string indexTemplateFilename =
334-
std::format("index.{}.hbs", domCorpus.fileExtension);
335-
std::string wrapperTemplateFilename =
336-
std::format("wrapper.{}.hbs", domCorpus.fileExtension);
337339
auto const layoutDirs = templateDirs(config, domCorpus.fileExtension, "layouts");
338-
for (std::string const& filename : {indexTemplateFilename, wrapperTemplateFilename})
340+
341+
auto loadLayoutTemplate = [&](std::string const& filename) -> Expected<void, Error>
339342
{
340343
bool loaded = false;
341344
for (auto const& dir : layoutDirs)
342345
{
343-
std::string pathName = files::appendPath(dir, filename);
346+
auto const pathName = files::appendPath(dir, filename);
344347
if (!files::exists(pathName))
345348
continue;
346-
auto text = files::getFileText(pathName);
347-
if (!text)
348-
{
349-
text.error().Throw();
350-
}
351-
templates_[filename] = std::move(text.value());
349+
MRDOCS_TRY(auto text, files::getFileText(pathName));
350+
templates_[filename] = std::move(text); // later dirs override
352351
loaded = true;
353352
}
354353
if (!loaded)
355354
{
356355
formatError("Template {} not found in addons search path", filename).Throw();
357356
}
358-
}
357+
return {};
358+
};
359+
360+
if (auto exp = loadLayoutTemplate(std::format("index.{}.hbs", domCorpus.fileExtension)); !exp)
361+
exp.error().Throw();
362+
if (auto exp = loadLayoutTemplate(std::format("wrapper.{}.hbs", domCorpus.fileExtension)); !exp)
363+
exp.error().Throw();
359364
}
360365

361366
//------------------------------------------------
@@ -503,35 +508,35 @@ std::string
503508
Builder::
504509
layoutDir() const
505510
{
506-
return pickTemplateDir(domCorpus->config, domCorpus.fileExtension, "layouts", "layout");
511+
return pickTemplateDir(domCorpus->config, domCorpus.fileExtension, "layouts", "layout", paths::Preference::LastExisting);
507512
}
508513

509514
std::string
510515
Builder::
511516
templatesDir() const
512517
{
513-
return pickTemplateDir(domCorpus->config, domCorpus.fileExtension, {}, "template");
518+
return pickTemplateDir(domCorpus->config, domCorpus.fileExtension, {}, "template", paths::Preference::LastExisting);
514519
}
515520

516521
std::string
517522
Builder::
518523
templatesDir(std::string_view subdir) const
519524
{
520-
return pickTemplateDir(domCorpus->config, domCorpus.fileExtension, subdir, "template");
525+
return pickTemplateDir(domCorpus->config, domCorpus.fileExtension, subdir, "template", paths::Preference::LastExisting);
521526
}
522527

523528
std::string
524529
Builder::
525530
commonTemplatesDir() const
526531
{
527-
return pickTemplateDir(domCorpus->config, "common", {}, "common template");
532+
return pickTemplateDir(domCorpus->config, "common", {}, "common template", paths::Preference::LastExisting);
528533
}
529534

530535
std::string
531536
Builder::
532537
commonTemplatesDir(std::string_view const subdir) const
533538
{
534-
return pickTemplateDir(domCorpus->config, "common", subdir, "common template");
539+
return pickTemplateDir(domCorpus->config, "common", subdir, "common template", paths::Preference::LastExisting);
535540
}
536541

537542

src/lib/Gen/hbs/Builder.hpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ namespace mrdocs {
2626
namespace hbs {
2727

2828
namespace paths {
29-
std::string selectExistingTemplateDir(std::vector<std::string> const& candidates);
29+
enum class Preference { FirstExisting, LastExisting };
30+
31+
std::string selectExistingTemplateDir(
32+
std::vector<std::string> const& candidates,
33+
Preference pref = Preference::LastExisting);
3034
}
3135

3236
/** Builds reference output as a string for any Info type

src/lib/Support/JavaScript.cpp

Lines changed: 61 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ std::mutex g_init_mtx;
2929
unsigned g_jerry_refcount = 0;
3030
}
3131

32+
bool
33+
isOptionsObject(dom::Value const& v)
34+
{
35+
if (!v.isObject())
36+
return false;
37+
// Handlebars options objects usually carry these markers.
38+
return static_cast<bool>(v.get("hash")) ||
39+
static_cast<bool>(v.get("data")) ||
40+
static_cast<bool>(v.get("fn")) ||
41+
static_cast<bool>(v.get("inverse"));
42+
}
43+
3244
static std::string toString(jerry_value_t v)
3345
{
3446
// Callers must hold the context lock.
@@ -204,6 +216,47 @@ void Value::swap(Value& other) noexcept
204216
static dom::Value toDomValue(jerry_value_t v, std::shared_ptr<Context::Impl> const& impl);
205217
static jerry_value_t toJsValue(dom::Value const& v, std::shared_ptr<Context::Impl> const& impl);
206218

219+
static Expected<Value, Error> resolveHelperFunction(
220+
Scope& scope,
221+
std::string_view name,
222+
std::string_view script)
223+
{
224+
if (auto exp = scope.eval(script))
225+
{
226+
if (exp->isFunction())
227+
return *exp;
228+
}
229+
else
230+
{
231+
return Unexpected(exp.error());
232+
}
233+
234+
std::string wrapped;
235+
wrapped.reserve(script.size() + 2);
236+
wrapped.push_back('(');
237+
wrapped.append(script);
238+
wrapped.push_back(')');
239+
240+
if (auto expr = scope.eval(wrapped))
241+
{
242+
if (expr->isFunction())
243+
return *expr;
244+
}
245+
else
246+
{
247+
return Unexpected(expr.error());
248+
}
249+
250+
if (Value global = scope.getGlobalObject())
251+
{
252+
Value candidate = global.get(name);
253+
if (candidate.isFunction())
254+
return candidate;
255+
}
256+
257+
return Unexpected(Error(std::string("helper is not a function: ") + std::string(name)));
258+
}
259+
207260
Type Value::type() const noexcept
208261
{
209262
if (!val_)
@@ -830,9 +883,7 @@ static dom::Value toDomValue(jerry_value_t v, std::shared_ptr<Context::Impl> con
830883
if (jerry_value_is_array(v))
831884
{
832885
dom::Array arr;
833-
jerry_value_t lenVal = jerry_object_get(v, jerry_string_sz("length"));
834-
uint32_t len = (uint32_t)jerry_value_as_number(lenVal);
835-
jerry_value_free(lenVal);
886+
uint32_t len = jerry_array_length(v);
836887
for (uint32_t i = 0; i < len; ++i)
837888
{
838889
jerry_value_t elem = jerry_object_get_index(v, i);
@@ -846,9 +897,7 @@ static dom::Value toDomValue(jerry_value_t v, std::shared_ptr<Context::Impl> con
846897
{
847898
dom::Object obj;
848899
jerry_value_t keys = jerry_object_keys(v);
849-
jerry_value_t lenVal = jerry_object_get(keys, jerry_string_sz("length"));
850-
uint32_t len = (uint32_t)jerry_value_as_number(lenVal);
851-
jerry_value_free(lenVal);
900+
uint32_t len = jerry_array_length(keys);
852901
for (uint32_t i = 0; i < len; ++i)
853902
{
854903
jerry_value_t key = jerry_object_get_index(keys, i);
@@ -877,33 +926,7 @@ registerHelper(
877926
std::string_view script)
878927
{
879928
Scope scope(ctx);
880-
auto exp = scope.eval(script);
881-
if (!exp)
882-
return Unexpected(exp.error());
883-
Value fn = *exp;
884-
if (!fn.isFunction())
885-
{
886-
// Try treating the script as a function expression by parenthesizing it.
887-
std::string wrapped("(");
888-
wrapped.append(script);
889-
wrapped.append(")");
890-
auto expr = scope.eval(wrapped);
891-
if (!expr)
892-
return Unexpected(expr.error());
893-
if (expr->isFunction())
894-
fn = *expr;
895-
}
896-
if (!fn.isFunction())
897-
{
898-
// Fallback: scripts that declare a named function without returning it
899-
// (e.g., `function helper() {}`) leave the function on the global object.
900-
// Look it up by the helper name to preserve the legacy helper lookup behavior.
901-
Value global = scope.getGlobalObject();
902-
if (global)
903-
fn = global.get(name);
904-
}
905-
if (!fn.isFunction())
906-
return Unexpected(Error(std::string("helper is not a function: ") + std::string(name)));
929+
MRDOCS_TRY(Value fn, resolveHelperFunction(scope, name, script));
907930

908931
// Store helper on global object (preserve existing helpers if present)
909932
Value helpers = scope.getGlobal("MrDocsHelpers").value_or(Value{});
@@ -919,10 +942,11 @@ registerHelper(
919942
{
920943
std::vector<dom::Value> vec(args.begin(), args.end());
921944

922-
// Handlebars passes an options object as the final argument; drop it so
923-
// helpers see only user-supplied parameters (matches legacy behavior).
924-
if (vec.size() >= 3)
925-
vec.resize(2);
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+
926950
auto ret = fn.call(vec);
927951
if (!ret) return Unexpected(ret.error());
928952
auto domRet = ret->getDom();

test-files/golden-tests/generator/hbs/js-helper/helpers.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
* string: string&colon;hi
88
* null: null&colon;
99
* undefined: undefined&colon;
10-
* array: array&colon;a,b
10+
* array: array&colon;a,b,3
1111
* hash: hash&colon;a&equals;1,b&equals;two
12-
* glue: x
12+
* glue: x&verbar;y&verbar;z
1313
* block: otherwise
1414
1515
[.small]#Created with https://www.mrdocs.com[MrDocs]#

test-files/golden-tests/generator/hbs/js-helper/helpers.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
<li id="string">string:hi</li>
99
<li id="null">null:</li>
1010
<li id="undefined">undefined:</li>
11-
<li id="array">array:a,b</li>
11+
<li id="array">array:a,b,3</li>
1212
<li id="hash">hash:a&#x3D;1,b&#x3D;two</li>
13-
<li id="glue">x</li>
13+
<li id="glue">x|y|z</li>
1414
<li id="block">otherwise</li>
1515
</ul>
1616
</body>

0 commit comments

Comments
 (0)