Skip to content

Commit 1a1b510

Browse files
committed
feat: complete JavaScript helpers support
This commit uses proxy objects to offer complete support for JavaScript helpers that return reference types. Previously, JavaScript reference types returned by these functions were deep-copied or not handled at all. Now, proxy objects in both directions are used while helpers create a scope that's kept alive as long as necessary by the Handlebars engine. As the objects don't need to be deep copied, this change improves performance and allows objects with circular references, which are common in MrDocs. Additionally, JavaScript helpers receive a proxy object equivalent to the handlebars `options` object, and helper function registration was also simplified and improved to remove redundant code. This commit provides new test cases to validate the current code without counting on MrDocs.
1 parent 5230768 commit 1a1b510

File tree

8 files changed

+493
-143
lines changed

8 files changed

+493
-143
lines changed

CMakeUserPresets.json.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@
6060
"Clang_ROOT": "C:\\Users\\$env{USERNAME}\\Libraries\\llvm-project\\llvm\\install\\MSVC\\RelWithDebInfo"
6161
}
6262
},
63+
{
64+
"name": "DebWithOpt-ClangCL",
65+
"inherits": "DebWithOpt-MSVC",
66+
"binaryDir": "${sourceDir}/build/${presetName}",
67+
"cacheVariables": {
68+
"CMAKE_C_COMPILER": "clang-cl.exe",
69+
"CMAKE_CXX_COMPILER": "clang-cl.exe"
70+
}
71+
},
6372
{
6473
"name": "Debug-GCC",
6574
"inherits": "debug",

include/mrdocs/Support/JavaScript.hpp

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020

2121
namespace clang {
2222
namespace mrdocs {
23+
24+
class Handlebars;
25+
2326
namespace js {
2427

2528
struct Access;
@@ -214,6 +217,42 @@ class Scope
214217
MRDOCS_DECL
215218
~Scope();
216219

220+
/** Push an integer to the stack
221+
*/
222+
MRDOCS_DECL
223+
Value
224+
pushInteger(std::int64_t value);
225+
226+
/** Push a double to the stack
227+
*/
228+
MRDOCS_DECL
229+
Value
230+
pushDouble(double value);
231+
232+
/** Push a boolean to the stack
233+
*/
234+
MRDOCS_DECL
235+
Value
236+
pushBoolean(bool value);
237+
238+
/** Push a string to the stack
239+
*/
240+
MRDOCS_DECL
241+
Value
242+
pushString(std::string_view value);
243+
244+
/** Push a new object to the stack
245+
*/
246+
MRDOCS_DECL
247+
Value
248+
pushObject();
249+
250+
/** Push a new array to the stack
251+
*/
252+
MRDOCS_DECL
253+
Value
254+
pushArray();
255+
217256
/** Compile and run a script.
218257
219258
This function compiles and executes
@@ -973,6 +1012,20 @@ isFunction() const noexcept
9731012
return type() == Type::function;
9741013
}
9751014

1015+
/** Register a JavaScript helper function
1016+
1017+
This function registers a JavaScript function
1018+
as a helper function that can be called from
1019+
Handlebars templates.
1020+
*/
1021+
MRDOCS_DECL
1022+
Expected<void, Error>
1023+
registerHelper(
1024+
clang::mrdocs::Handlebars& hbs,
1025+
std::string_view name,
1026+
Context& ctx,
1027+
std::string_view script);
1028+
9761029
} // js
9771030
} // mrdocs
9781031
} // clang

include/mrdocs/Support/Path.hpp

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -44,32 +44,80 @@ forEachFile(
4444
bool recursive,
4545
AnyFileVisitor& visitor);
4646

47-
/** Visit each file in a directory.
48-
*/
49-
template<class Visitor>
50-
Error
51-
forEachFile(
52-
std::string_view dirPath,
53-
bool recursive,
54-
Visitor&& visitor)
47+
namespace detail {
48+
template <class Visitor>
49+
struct FileVisitor : AnyFileVisitor
5550
{
56-
struct FileVisitor : AnyFileVisitor
51+
Visitor& visitor_;
52+
53+
explicit FileVisitor(Visitor& v)
54+
: visitor_(v)
5755
{
58-
Visitor& visitor_;
56+
}
5957

60-
explicit FileVisitor(Visitor& v)
61-
: visitor_(v)
58+
Error
59+
visitFile(std::string_view fileName) override
60+
{
61+
using R = std::invoke_result_t<Visitor, std::string_view>;
62+
if (std::same_as<R, void>)
63+
{
64+
visitor_(fileName);
65+
return Error::success();
66+
}
67+
else
6268
{
69+
return toError(visitor_(fileName));
6370
}
71+
}
72+
73+
static
74+
Error
75+
toError(Expected<void, Error> const& e)
76+
{
77+
return e ? Error::success() : e.error();
78+
}
79+
80+
template <class T>
81+
static
82+
Error
83+
toError(Expected<T, Error> const& e)
84+
{
85+
return e ? toError(e.value()) : e.error();
86+
}
6487

65-
Error
66-
visitFile(std::string_view fileName) override
88+
template <class T>
89+
static
90+
Error
91+
toError(T const& e)
92+
{
93+
if constexpr (std::same_as<T, Error>)
94+
{
95+
return e;
96+
}
97+
else if constexpr (std::convertible_to<T, bool>)
98+
{
99+
if (e)
100+
return Error::success();
101+
return Error("visitor returned falsy");
102+
}
103+
else
67104
{
68-
return visitor_(fileName);
105+
return Error::success();
69106
}
70-
};
107+
}
108+
};
109+
}
71110

72-
FileVisitor v{visitor};
111+
/** Visit each file in a directory.
112+
*/
113+
template<class Visitor>
114+
Error
115+
forEachFile(
116+
std::string_view dirPath,
117+
bool recursive,
118+
Visitor&& visitor)
119+
{
120+
detail::FileVisitor<Visitor> v{visitor};
73121
return forEachFile(dirPath, recursive,
74122
static_cast<AnyFileVisitor&>(v));
75123
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
function add(a, b) {
2+
return a + b;
3+
}

src/lib/Gen/adoc/Builder.cpp

Lines changed: 9 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -57,75 +57,28 @@ Builder(
5757
return Error::success();
5858
}).maybeThrow();
5959

60-
// load helpers
61-
js::Scope scope(ctx_);
60+
// Load JavaScript helpers
6261
std::string helpersPath = files::appendPath(
6362
config->addonsDir, "generator", "asciidoc", "helpers");
6463
forEachFile(helpersPath, true,
65-
[&](std::string_view pathName)
64+
[&](std::string_view pathName)-> Expected<void>
6665
{
6766
// Register JS helper function in the global object
6867
constexpr std::string_view ext = ".js";
69-
if (!pathName.ends_with(ext))
70-
{
71-
return Error::success();
72-
}
68+
if (!pathName.ends_with(ext)) return {};
7369
auto name = files::getFileName(pathName);
7470
name.remove_suffix(ext.size());
75-
auto text = files::getFileText(pathName);
76-
if (!text)
77-
{
78-
return text.error();
79-
}
80-
auto JSFn = scope.compile_function(*text);
81-
if (!JSFn)
82-
{
83-
return JSFn.error();
84-
}
85-
scope.getGlobalObject().set(name, *JSFn);
86-
87-
// Register C++ helper that retrieves the helper
88-
// from the global object, converts the arguments,
89-
// and invokes the JS function.
90-
hbs_.registerHelper(name, dom::makeVariadicInvocable(
91-
[this, name=std::string(name)](
92-
dom::Array const& args) -> Expected<dom::Value>
93-
{
94-
// Get function from global scope
95-
js::Scope scope(ctx_);
96-
js::Value fn = scope.getGlobalObject().get(name);
97-
if (fn.isUndefined())
98-
{
99-
return Unexpected(Error("helper not found"));
100-
}
101-
if (!fn.isFunction())
102-
{
103-
return Unexpected(Error("helper is not a function"));
104-
}
105-
106-
// Call function
107-
std::vector<dom::Value> arg_span;
108-
arg_span.reserve(args.size());
109-
for (auto const& arg : args)
110-
{
111-
arg_span.push_back(arg);
112-
}
113-
auto result = fn.apply(arg_span);
114-
if (!result)
115-
{
116-
return dom::Kind::Undefined;
117-
}
118-
119-
// Convert result to dom::Value
120-
return result->getDom();
121-
}));
122-
return Error::success();
71+
MRDOCS_TRY(auto script, files::getFileText(pathName));
72+
MRDOCS_TRY(js::registerHelper(hbs_, name, ctx_, script));
73+
return {};
12374
}).maybeThrow();
75+
12476
hbs_.registerHelper(
12577
"is_multipage",
12678
dom::makeInvocable([res = config->multiPage]() -> Expected<dom::Value> {
12779
return res;
12880
}));
81+
12982
hbs_.registerHelper("primary_location",
13083
dom::makeInvocable([](dom::Value const& v) ->
13184
dom::Value
@@ -162,6 +115,7 @@ Builder(
162115
}
163116
return first;
164117
}));
118+
165119
helpers::registerStringHelpers(hbs_);
166120
helpers::registerAntoraHelpers(hbs_);
167121
helpers::registerContainerHelpers(hbs_);
@@ -183,8 +137,6 @@ callTemplate(
183137
MRDOCS_TRY(auto fileText, files::getFileText(pathName));
184138
HandlebarsOptions options;
185139
options.noEscape = true;
186-
// options.compat = true;
187-
188140
Expected<std::string, HandlebarsError> exp =
189141
hbs_.try_render(fileText, context, options);
190142
if (!exp)

src/lib/Gen/html/Builder.cpp

Lines changed: 14 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -60,70 +60,23 @@ Builder(
6060
return Error::success();
6161
}).maybeThrow();
6262

63-
// load helpers
64-
js::Scope scope(ctx_);
63+
// Load JavaScript helpers
6564
std::string helpersPath = files::appendPath(
66-
config->addonsDir, "generator", "asciidoc", "helpers");
65+
config->addonsDir, "generator", "html", "helpers");
6766
forEachFile(helpersPath, true,
68-
[&](std::string_view pathName)
69-
{
70-
// Register JS helper function in the global object
71-
constexpr std::string_view ext = ".js";
72-
if (!pathName.ends_with(ext))
73-
{
74-
return Error::success();
75-
}
76-
auto name = files::getFileName(pathName);
77-
name.remove_suffix(ext.size());
78-
auto text = files::getFileText(pathName);
79-
if (!text)
67+
[&](std::string_view pathName)-> Expected<void>
8068
{
81-
return text.error();
82-
}
83-
auto JSFn = scope.compile_function(*text);
84-
if (!JSFn)
85-
{
86-
return JSFn.error();
87-
}
88-
scope.getGlobalObject().set(name, *JSFn);
89-
90-
// Register C++ helper that retrieves the helper
91-
// from the global object, converts the arguments,
92-
// and invokes the JS function.
93-
hbs_.registerHelper(name, dom::makeVariadicInvocable(
94-
[this, name=std::string(name)](
95-
dom::Array const& args) -> Expected<dom::Value>
96-
{
97-
// Get function from global scope
98-
js::Scope scope(ctx_);
99-
js::Value fn = scope.getGlobalObject().get(name);
100-
if (fn.isUndefined())
101-
{
102-
return Unexpected(Error("helper not found"));
103-
}
104-
if (!fn.isFunction())
105-
{
106-
return Unexpected(Error("helper is not a function"));
107-
}
108-
109-
// Call function
110-
std::vector<dom::Value> arg_span;
111-
arg_span.reserve(args.size());
112-
for (auto const& arg : args)
113-
{
114-
arg_span.push_back(arg);
115-
}
116-
auto result = fn.apply(arg_span);
117-
if (!result)
118-
{
119-
return dom::Kind::Undefined;
120-
}
121-
122-
// Convert result to dom::Value
123-
return result->getDom();
124-
}));
125-
return Error::success();
126-
}).maybeThrow();
69+
// Register JS helper function in the global object
70+
constexpr std::string_view ext = ".js";
71+
if (!pathName.ends_with(ext))
72+
return {};
73+
auto name = files::getFileName(pathName);
74+
name.remove_suffix(ext.size());
75+
MRDOCS_TRY(auto script, files::getFileText(pathName));
76+
MRDOCS_TRY(js::registerHelper(hbs_, name, ctx_, script));
77+
return {};
78+
}).maybeThrow();
79+
12780
hbs_.registerHelper(
12881
"is_multipage",
12982
dom::makeInvocable([res = config->multiPage]() -> Expected<dom::Value> {

0 commit comments

Comments
 (0)