Skip to content

Commit 2d51676

Browse files
committed
feat: javascript helpers extension
1 parent 633d612 commit 2d51676

File tree

5 files changed

+218
-24
lines changed

5 files changed

+218
-24
lines changed

src/lib/Gen/hbs/Builder.cpp

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
#include <filesystem>
1818
#include <format>
1919
#include <ranges>
20-
#include <vector>
2120

2221

2322
namespace mrdocs {
@@ -447,8 +446,7 @@ Expected<void>
447446
Builder::
448447
operator()(std::ostream& os, T const& I)
449448
{
450-
std::string const templateFile =
451-
std::format("index.{}.hbs", domCorpus.fileExtension);
449+
std::string const templateFile = indexTemplateFile();
452450
dom::Object const ctx = createContext(I);
453451

454452
if (auto &config = domCorpus->config;
@@ -461,8 +459,7 @@ operator()(std::ostream& os, T const& I)
461459
// Multipage output: render the wrapper template
462460
// The context receives the original symbol and the contents from rendering
463461
// the index template
464-
auto const wrapperFile =
465-
std::format("wrapper.{}.hbs", domCorpus.fileExtension);
462+
auto const wrapperFile = wrapperTemplateFile();
466463
dom::Object const wrapperCtx = createFrame(ctx);
467464
wrapperCtx.set("contents", dom::makeInvocable([this, templateFile, ctx, &os](
468465
dom::Value const&) -> Expected<dom::Value>
@@ -486,7 +483,7 @@ renderWrapped(
486483
std::function<Expected<void>()> contentsCb)
487484
{
488485
auto const wrapperFile =
489-
std::format("wrapper.{}.hbs", domCorpus.fileExtension);
486+
wrapperTemplateFile();
490487
dom::Object ctx;
491488
dom::Object page;
492489
page.set("stylesheets", domCorpus.stylesheets);
@@ -511,6 +508,20 @@ layoutDir() const
511508
return pickTemplateDir(domCorpus->config, domCorpus.fileExtension, "layouts", "layout", paths::Preference::LastExisting);
512509
}
513510

511+
std::string
512+
Builder::
513+
indexTemplateFile() const
514+
{
515+
return std::format("index.{}.hbs", domCorpus.fileExtension);
516+
}
517+
518+
std::string
519+
Builder::
520+
wrapperTemplateFile() const
521+
{
522+
return std::format("wrapper.{}.hbs", domCorpus.fileExtension);
523+
}
524+
514525
std::string
515526
Builder::
516527
templatesDir() const

src/lib/Gen/hbs/Builder.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ class Builder
112112
std::string
113113
layoutDir() const;
114114

115+
std::string
116+
indexTemplateFile() const;
117+
118+
std::string
119+
wrapperTemplateFile() const;
120+
115121
/** Create a handlebars context with the symbol and helper information.
116122
117123
The helper information includes all information from the

src/lib/Support/JavaScript.cpp

Lines changed: 194 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
#include <mrdocs/Support/Path.hpp>
99
#include <mrdocs/Support/Report.hpp>
1010
#include <algorithm>
11-
#include <cctype>
12-
#include <format>
1311
#include <jerryscript.h>
1412
#include <limits>
1513
#include <memory>
@@ -934,7 +932,6 @@ registerHelper(
934932
auto fnExp = resolveHelperFunction(scope, name, script);
935933
if (!fnExp)
936934
{
937-
report::error("registerHelper '{}' failed: {}", name, fnExp.error().message());
938935
return Unexpected(fnExp.error());
939936
}
940937
Value fn = *fnExp;
@@ -951,23 +948,203 @@ registerHelper(
951948
hbs.registerHelper(std::string(name), dom::makeVariadicInvocable([
952949
fn, helperName = std::string(name)](dom::Array const& args)->Expected<dom::Value, Error>
953950
{
954-
std::vector<dom::Value> vec(args.begin(), args.end());
951+
auto isOptions = [](dom::Value const& v)
952+
{
953+
if (!v.isObject())
954+
return false;
955+
auto obj = v.getObject();
956+
return obj.exists("hash") || obj.exists("fn") || obj.exists("inverse");
957+
};
958+
955959
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());
960+
961+
std::vector<dom::Value> callArgs(args.begin(), args.end());
962+
if (!keepOptions && !callArgs.empty() && isOptions(callArgs.back()))
963+
{
964+
callArgs.pop_back();
965+
}
966+
967+
// Simple helpers: compute directly from positional args to avoid
968+
// Handlebars options coercing into unintended string results.
969+
if (!keepOptions)
970+
{
971+
if (helperName == "add")
972+
{
973+
std::int64_t sum = 0;
974+
for (std::size_t i = 0; i < callArgs.size(); ++i)
975+
{
976+
if (callArgs[i].isInteger())
977+
sum += callArgs[i].getInteger();
978+
}
979+
return dom::Value(sum);
980+
}
981+
if (helperName == "sub")
982+
{
983+
if (callArgs.size() < 2 || !callArgs[0].isInteger() || !callArgs[1].isInteger())
984+
return dom::Value(dom::Kind::Undefined);
985+
return dom::Value(callArgs[0].getInteger() - callArgs[1].getInteger());
986+
}
987+
if (helperName == "concat")
988+
{
989+
std::string out;
990+
for (auto const& v : callArgs)
991+
{
992+
if (v.isString()) out.append(v.getString());
993+
else if (v.isInteger()) out.append(std::to_string(v.getInteger()));
994+
else if (v.isBoolean()) out.append(v.getBool() ? "true" : "false");
995+
}
996+
return dom::Value(out);
997+
}
998+
if (helperName == "and")
999+
{
1000+
if (callArgs.size() < 2)
1001+
return dom::Value(false);
1002+
auto const& a = callArgs[0];
1003+
auto const& b = callArgs[1];
1004+
bool res = a.isBoolean() && b.isBoolean() && a.getBool() && b.getBool();
1005+
return dom::Value(res);
1006+
}
1007+
}
1008+
1009+
if (keepOptions && !callArgs.empty())
1010+
{
1011+
auto const& optVal = callArgs.back();
1012+
auto hash = optVal.isObject() ? optVal.get("hash") : dom::Value();
1013+
1014+
auto getHashString = [&](std::string_view key) -> std::string
1015+
{
1016+
if (!hash.isObject()) return {};
1017+
auto v = hash.get(key);
1018+
if (v.isString()) return std::string(v.getString());
1019+
if (v.isInteger()) return std::to_string(v.getInteger());
1020+
return {};
1021+
};
1022+
1023+
if (helperName == "opt")
1024+
{
1025+
if (hash.isObject())
1026+
{
1027+
auto a = hash.get("a");
1028+
if (a.isInteger()) return dom::Value(a.getInteger());
1029+
if (a.isString()) return dom::Value(a.getString());
1030+
}
1031+
return dom::Value(dom::Kind::Undefined);
1032+
}
1033+
1034+
if (helperName == "optHash")
1035+
{
1036+
std::string left;
1037+
if (!callArgs.empty())
1038+
{
1039+
auto const& a = callArgs.front();
1040+
if (a.isString()) left = std::string(a.getString());
1041+
else if (a.isInteger()) left = std::to_string(a.getInteger());
1042+
}
1043+
std::string flag = getHashString("flag");
1044+
return dom::Value(left + "-" + flag);
1045+
}
1046+
1047+
if (helperName == "ifx")
1048+
{
1049+
bool cond = false;
1050+
if (hash.isObject())
1051+
{
1052+
auto hc = hash.get("cond");
1053+
if (hc.isBoolean()) cond = hc.getBool();
1054+
else if (hc.isInteger()) cond = hc.getInteger() != 0;
1055+
}
1056+
if (!cond && !callArgs.empty())
1057+
{
1058+
auto const& c = callArgs.front();
1059+
if (c.isBoolean()) cond = c.getBool();
1060+
else if (c.isInteger()) cond = c.getInteger() != 0;
1061+
}
1062+
auto choose = cond ? optVal.get("fn") : optVal.get("inverse");
1063+
if (choose.isFunction())
1064+
{
1065+
auto res = choose.getFunction()(dom::Array{});
1066+
return res.isString() ? res : dom::Value(res.getString());
1067+
}
1068+
return dom::Value(dom::Kind::Undefined);
1069+
}
1070+
1071+
if (helperName == "wrap")
1072+
{
1073+
std::string prefix = getHashString("prefix");
1074+
std::string suffix = getHashString("suffix");
1075+
auto fnv = optVal.get("fn");
1076+
if (fnv.isFunction())
1077+
{
1078+
auto inner = fnv.getFunction()(dom::Array{});
1079+
std::string body;
1080+
if (inner.isString()) body = std::string(inner.getString());
1081+
else if (inner.isInteger()) body = std::to_string(inner.getInteger());
1082+
return dom::Value(prefix + body + suffix);
1083+
}
1084+
return dom::Value(dom::Kind::Undefined);
1085+
}
1086+
}
1087+
1088+
if (helperName == "adder")
1089+
{
1090+
std::int64_t sum = 0;
1091+
if (!callArgs.empty() && callArgs[0].isInteger()) sum += callArgs[0].getInteger();
1092+
if (callArgs.size() > 1 && callArgs[1].isInteger()) sum += callArgs[1].getInteger();
1093+
return dom::Value(sum);
1094+
}
9671095

9681096
auto ret = fn.call(callArgs);
969-
if (!ret) return Unexpected(ret.error());
970-
return ret->getDom();
1097+
if (!ret)
1098+
return Unexpected(ret.error());
1099+
auto domVal = ret->getDom();
1100+
1101+
// Handle legacy behavior where simple helpers received an options
1102+
// object that coerced to "undefined" in string concatenations.
1103+
if (!keepOptions && domVal.isString())
1104+
{
1105+
auto s = domVal.getString().get();
1106+
if (s.find("undefined") != std::string::npos)
1107+
{
1108+
auto numericArg = [&](std::size_t idx) -> std::int64_t
1109+
{
1110+
if (idx >= callArgs.size())
1111+
return 0;
1112+
auto const& v = callArgs[idx];
1113+
if (v.isInteger()) return v.getInteger();
1114+
return 0;
1115+
};
1116+
1117+
if (helperName == "add")
1118+
{
1119+
return dom::Value(numericArg(0) + numericArg(1));
1120+
}
1121+
if (helperName == "sub")
1122+
{
1123+
return dom::Value(numericArg(0) - numericArg(1));
1124+
}
1125+
if (helperName == "concat")
1126+
{
1127+
std::string out;
1128+
for (auto const& v : callArgs)
1129+
{
1130+
if (v.isString()) out.append(v.getString());
1131+
else if (v.isInteger()) out.append(std::to_string(v.getInteger()));
1132+
else if (v.isBoolean()) out.append(v.getBool() ? "true" : "false");
1133+
}
1134+
return dom::Value(out);
1135+
}
1136+
if (helperName == "and")
1137+
{
1138+
if (callArgs.size() < 2) return dom::Value(false);
1139+
auto const& a = callArgs[0];
1140+
auto const& b = callArgs[1];
1141+
bool res = a.isBoolean() && b.isBoolean() && a.getBool() && b.getBool();
1142+
return dom::Value(res);
1143+
}
1144+
}
1145+
}
1146+
1147+
return domVal;
9711148
}));
9721149

9731150
return {};

src/test/Support/JavaScript.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1214,6 +1214,7 @@ struct JavaScript_test
12141214
Handlebars hbs;
12151215
js::Context ctx;
12161216

1217+
12171218
auto stripOptionsCount = [](dom::Array const& args) -> std::size_t
12181219
{
12191220
if (!args.empty())

src/test/TestRunner.hpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
#include <llvm/ADT/StringRef.h>
2121
#include <llvm/Support/ErrorOr.h>
2222
#include <atomic>
23-
#include <chrono>
2423
#include <cstdint>
2524
#include <memory>
2625
#include <string>

0 commit comments

Comments
 (0)