From 63ac382438b6fa78041210f67f0736d0977a924b Mon Sep 17 00:00:00 2001 From: alandefreitas Date: Thu, 5 Oct 2023 15:57:39 -0300 Subject: [PATCH] refactor: MrDox uses C++ handlebars This PR refactors the generators to use the C++ implementation of handlebars. Javascript helpers are loaded with duktape. --- include/mrdox/Support/JavaScript.hpp | 49 ++++- .../addons/generator/asciidoc/helpers/add.js | 3 + src/lib/-HTML/Builder.cpp | 204 +++++++++--------- src/lib/-HTML/Builder.hpp | 2 + src/lib/-adoc/AdocGenerator.cpp | 6 +- src/lib/-adoc/Builder.cpp | 183 ++++++++-------- src/lib/-adoc/Builder.hpp | 2 + src/lib/Support/JavaScript.cpp | 115 +++++++++- 8 files changed, 352 insertions(+), 212 deletions(-) create mode 100644 share/mrdox/addons/generator/asciidoc/helpers/add.js diff --git a/include/mrdox/Support/JavaScript.hpp b/include/mrdox/Support/JavaScript.hpp index 9d9e050dc..baa5dd72d 100644 --- a/include/mrdox/Support/JavaScript.hpp +++ b/include/mrdox/Support/JavaScript.hpp @@ -16,6 +16,7 @@ #include #include #include +#include namespace clang { namespace mrdox { @@ -42,7 +43,9 @@ enum class Type boolean, number, string, - object + object, + function, + array }; //------------------------------------------------ @@ -120,12 +123,18 @@ class Scope MRDOX_DECL ~Scope(); - /** Run a script. + /** Compile and run a script. */ MRDOX_DECL Error script(std::string_view jsCode); + /** Compile a script and push results to stack. + */ + MRDOX_DECL + Expected + compile(std::string_view jsCode); + /** Return the global object. */ MRDOX_DECL @@ -170,10 +179,12 @@ class Value bool isString() const noexcept; bool isArray() const noexcept; bool isObject() const noexcept; + bool isFunction() const noexcept; std::string getString() const; + dom::Value getDom() const; -void setlog(); + void setlog(); /** Call a function. */ @@ -184,6 +195,15 @@ void setlog(); return callImpl({ dom::Value(std::forward(args))... }); } + /** Call a function with variadic arguments. + */ + template + Expected + apply(std::span args) const + { + return callImpl(args); + } + /** Call a function. */ template @@ -205,12 +225,30 @@ void setlog(); { dom::Value(std::forward(args))... }); } + /** Set a key. + */ + MRDOX_DECL + void + set( + std::string_view key, + Value value) const; + + /** Get a key. + */ + MRDOX_DECL + Value + get(std::string_view key) const; + private: MRDOX_DECL Expected callImpl( std::initializer_list args) const; + MRDOX_DECL + Expected + callImpl(std::span args) const; + MRDOX_DECL Expected callPropImpl( @@ -248,6 +286,11 @@ inline bool Value::isObject() const noexcept return type() == Type::object; } +inline bool Value::isFunction() const noexcept +{ + return type() == Type::function; +} + } // js } // mrdox } // clang diff --git a/share/mrdox/addons/generator/asciidoc/helpers/add.js b/share/mrdox/addons/generator/asciidoc/helpers/add.js new file mode 100644 index 000000000..85f080e7b --- /dev/null +++ b/share/mrdox/addons/generator/asciidoc/helpers/add.js @@ -0,0 +1,3 @@ +function add(a, b) { + return a + b; +} \ No newline at end of file diff --git a/src/lib/-HTML/Builder.cpp b/src/lib/-HTML/Builder.cpp index bb476504c..19719e646 100644 --- a/src/lib/-HTML/Builder.cpp +++ b/src/lib/-HTML/Builder.cpp @@ -38,112 +38,105 @@ Builder( Config const& config = corpus_.config; - js::Scope scope(ctx_); - - scope.script(files::getFileText( - files::appendPath( - config->addonsDir, "js", "handlebars.js") - ).value()).maybeThrow(); - auto Handlebars = scope.getGlobal("Handlebars").value(); - -// VFALCO refactor this -Handlebars.setlog(); - - // load templates -#if 0 - err = forEachFile(options_.template_dir, - [&](std::string_view fileName) + // load partials + std::string partialsPath = files::appendPath( + config->addonsDir, "generator", "html", "partials"); + forEachFile(partialsPath, + [&](std::string_view pathName) -> Error + { + constexpr std::string_view ext = ".html.hbs"; + if (!pathName.ends_with(ext)) { - if(! fileName.ends_with(".html.hbs")) - return Error::success(); return Error::success(); - }); -#endif - - Error err; - - // load partials - forEachFile( - files::appendPath(config->addonsDir, - "generator", "html", "partials"), - [&](std::string_view pathName) + } + auto name = files::getFileName(pathName); + name.remove_suffix(ext.size()); + auto text = files::getFileText(pathName); + if (!text) { - constexpr std::string_view ext = ".html.hbs"; - if(! pathName.ends_with(ext)) - return Error::success(); - auto name = files::getFileName(pathName); - name.remove_suffix(ext.size()); - auto text = files::getFileText(pathName); - if(! text) - return text.error(); - return Handlebars.callProp( - "registerPartial", name, *text) - .error_or(Error::success()); - }).maybeThrow(); + return text.error(); + } + hbs_.registerPartial(name, *text); + return Error::success(); + }).maybeThrow(); // load helpers -#if 0 - err = forEachFile( - files::appendPath(config->addonsDir, - "generator", "js", "helpers"), - [&](std::string_view pathName) + js::Scope scope(ctx_); + std::string helpersPath = files::appendPath( + config->addonsDir, "generator", "asciidoc", "helpers"); + forEachFile(helpersPath, + [&](std::string_view pathName) + { + // Register JS helper function in the global object + constexpr std::string_view ext = ".js"; + if (!pathName.ends_with(ext)) { - constexpr std::string_view ext = ".js"; - if(! pathName.ends_with(ext)) - return Error::success(); - auto name = files::getFileName(pathName); - name.remove_suffix(ext.size()); - //Handlebars.callProp("registerHelper", name, *text); - auto err = ctx_.scriptFromFile(pathName); return Error::success(); - }).maybeThrow(); -#endif - - scope.script(fmt::format( - R"(Handlebars.registerHelper( - 'is_multipage', function() - {{ - return {}; - }}); - )", config->multiPage)).maybeThrow(); - - scope.script(R"( - Handlebars.registerHelper( - 'to_string', function(context, depth) - { - return JSON.stringify(context, null, 2); - }); - - Handlebars.registerHelper( - 'eq', function(a, b) + } + auto name = files::getFileName(pathName); + name.remove_suffix(ext.size()); + auto text = files::getFileText(pathName); + if (!text) { - return a === b; - }); - - Handlebars.registerHelper( - 'neq', function(a, b) + return text.error(); + } + auto JSFn = scope.compile(*text); + if (!JSFn) { - return a !== b; - }); - - Handlebars.registerHelper( - 'not', function(a) - { - return ! a; - }); - - Handlebars.registerHelper( - 'or', function(a, b) + return JSFn.error(); + } + scope.getGlobalObject().set(name, *JSFn); + + // Register C++ helper that retrieves the helper + // from the global object, converts the arguments, + // and invokes the JS function. + hbs_.registerHelper(name, dom::makeVariadicInvocable( + [this, name=std::string(name)]( + dom::Array const& args) -> Expected { - return a || b; - }); - - Handlebars.registerHelper( - 'and', function(a, b) - { - return a && b; - }); - )").maybeThrow(); + // Get function from global scope + js::Scope scope(ctx_); + js::Value fn = scope.getGlobalObject().get(name); + if (fn.isUndefined()) + { + return Unexpected(Error("helper not found")); + } + if (!fn.isFunction()) + { + return Unexpected(Error("helper is not a function")); + } + + // Call function + std::vector arg_span; + arg_span.reserve(args.size()); + for (auto const& arg : args) + { + arg_span.push_back(arg); + } + auto result = fn.apply(arg_span); + if (!result) + { + return dom::Kind::Undefined; + } + + // Convert result to dom::Value + return result->getDom(); + })); + return Error::success(); + }).maybeThrow(); + hbs_.registerHelper( + "is_multipage", + dom::makeInvocable([res = config->multiPage]() -> Expected { + return res; + })); + helpers::registerAntoraHelpers(hbs_); + hbs_.registerHelper("neq", dom::makeVariadicInvocable(helpers::ne_fn)); + hbs_.registerHelper( + "to_string", + dom::makeInvocable( + [](dom::Value const& context) -> Expected { + return dom::JSON::stringify(context); + })); } //------------------------------------------------ @@ -157,18 +150,23 @@ callTemplate( Config const& config = corpus_.config; js::Scope scope(ctx_); + + auto Handlebars = scope.getGlobal("Handlebars"); auto layoutDir = files::appendPath(config->addonsDir, "generator", "html", "layouts"); auto pathName = files::appendPath(layoutDir, name); MRDOX_TRY(auto fileText, files::getFileText(pathName)); - dom::Object options; - options.set("allowProtoPropertiesByDefault", true); - // VFALCO This makes Proxy objects stop working - //options.set("allowProtoMethodsByDefault", true); - MRDOX_TRY(auto templateFn, Handlebars->callProp("compile", fileText, options)); - MRDOX_TRY(auto result, templateFn.call(context, options)); - return result.getString(); + HandlebarsOptions options; + options.noEscape = true; + + Expected exp = + hbs_.try_render(fileText, context, options); + if (!exp) + { + return Unexpected(Error(exp.error().what())); + } + return *exp; } Expected diff --git a/src/lib/-HTML/Builder.hpp b/src/lib/-HTML/Builder.hpp index 1d0e8a372..05f5653b6 100644 --- a/src/lib/-HTML/Builder.hpp +++ b/src/lib/-HTML/Builder.hpp @@ -15,6 +15,7 @@ #include "lib/Support/Radix.hpp" #include #include +#include #include #include @@ -33,6 +34,7 @@ class Builder Corpus const& corpus_; Options options_; js::Context ctx_; + Handlebars hbs_; public: Builder( diff --git a/src/lib/-adoc/AdocGenerator.cpp b/src/lib/-adoc/AdocGenerator.cpp index d5d5309d2..4c0bc2dc3 100644 --- a/src/lib/-adoc/AdocGenerator.cpp +++ b/src/lib/-adoc/AdocGenerator.cpp @@ -103,13 +103,13 @@ buildOne( }); errors = ex->wait(); if(! errors.empty()) - return Error(errors); + return {errors}; SinglePageVisitor visitor(*ex, corpus, os); visitor(corpus.globalNamespace()); errors = ex->wait(); if(! errors.empty()) - return Error(errors); + return {errors}; ex->async( [&os](Builder& builder) @@ -119,7 +119,7 @@ buildOne( }); errors = ex->wait(); if(! errors.empty()) - return Error(errors); + return {errors}; return Error::success(); } diff --git a/src/lib/-adoc/Builder.cpp b/src/lib/-adoc/Builder.cpp index a0a6e7ced..14140087a 100644 --- a/src/lib/-adoc/Builder.cpp +++ b/src/lib/-adoc/Builder.cpp @@ -35,110 +35,105 @@ Builder( Config const& config = domCorpus.getCorpus().config; - js::Scope scope(ctx_); - - scope.script(files::getFileText( - files::appendPath( - config->addonsDir, "js", "handlebars.js") - ).value()).maybeThrow(); - auto Handlebars = scope.getGlobal("Handlebars").value(); - -// VFALCO refactor this -Handlebars.setlog(); - - // load templates -#if 0 - err = forEachFile(options_.template_dir, - [&](std::string_view fileName) - { - if(! fileName.ends_with(".adoc.hbs")) - return Error::success(); - return Error::success(); - }); -#endif - - Error err; - // load partials - forEachFile( - files::appendPath(config->addonsDir, - "generator", "asciidoc", "partials"), + std::string partialsPath = files::appendPath( + config->addonsDir, "generator", "asciidoc", "partials"); + forEachFile(partialsPath, [&](std::string_view pathName) -> Error { constexpr std::string_view ext = ".adoc.hbs"; - if(! pathName.ends_with(ext)) + if (!pathName.ends_with(ext)) + { return Error::success(); + } auto name = files::getFileName(pathName); name.remove_suffix(ext.size()); auto text = files::getFileText(pathName); - if(! text) + if (!text) + { return text.error(); - return Handlebars.callProp("registerPartial", name, *text) - .error_or(Error::success()); + } + hbs_.registerPartial(name, *text); + return Error::success(); }).maybeThrow(); // load helpers -#if 0 - err = forEachFile( - files::appendPath(config->addonsDir, - "generator", "js", "helpers"), + js::Scope scope(ctx_); + std::string helpersPath = files::appendPath( + config->addonsDir, "generator", "asciidoc", "helpers"); + forEachFile(helpersPath, [&](std::string_view pathName) { + // Register JS helper function in the global object constexpr std::string_view ext = ".js"; - if(! pathName.ends_with(ext)) + if (!pathName.ends_with(ext)) + { return Error::success(); + } auto name = files::getFileName(pathName); name.remove_suffix(ext.size()); - //Handlebars.callProp("registerHelper", name, *text); - auto err = ctx_.scriptFromFile(pathName); + auto text = files::getFileText(pathName); + if (!text) + { + return text.error(); + } + auto JSFn = scope.compile(*text); + if (!JSFn) + { + return JSFn.error(); + } + scope.getGlobalObject().set(name, *JSFn); + + // Register C++ helper that retrieves the helper + // from the global object, converts the arguments, + // and invokes the JS function. + hbs_.registerHelper(name, dom::makeVariadicInvocable( + [this, name=std::string(name)]( + dom::Array const& args) -> Expected + { + // Get function from global scope + js::Scope scope(ctx_); + js::Value fn = scope.getGlobalObject().get(name); + if (fn.isUndefined()) + { + return Unexpected(Error("helper not found")); + } + if (!fn.isFunction()) + { + return Unexpected(Error("helper is not a function")); + } + + // Call function + std::vector arg_span; + arg_span.reserve(args.size()); + for (auto const& arg : args) + { + arg_span.push_back(arg); + } + auto result = fn.apply(arg_span); + if (!result) + { + return dom::Kind::Undefined; + } + + // Convert result to dom::Value + return result->getDom(); + })); return Error::success(); }).maybeThrow(); -#endif - scope.script(fmt::format( - R"(Handlebars.registerHelper( - 'is_multipage', function() - {{ - return {}; - }}); - )", config->multiPage)).maybeThrow(); - - scope.script(R"( - Handlebars.registerHelper( - 'to_string', function(context, depth) - { - return JSON.stringify(context, null, 2); - }); - - Handlebars.registerHelper( - 'eq', function(a, b) - { - return a === b; - }); - - Handlebars.registerHelper( - 'neq', function(a, b) - { - return a !== b; - }); - - Handlebars.registerHelper( - 'not', function(a) - { - return ! a; - }); - - Handlebars.registerHelper( - 'or', function(a, b) - { - return a || b; - }); - - Handlebars.registerHelper( - 'and', function(a, b) - { - return a && b; - }); - )").maybeThrow(); + hbs_.registerHelper( + "is_multipage", + dom::makeInvocable([res = config->multiPage]() -> Expected { + return res; + })); + helpers::registerAntoraHelpers(hbs_); + hbs_.registerHelper("neq", dom::makeVariadicInvocable(helpers::ne_fn)); + hbs_.registerHelper( + "to_string", + dom::makeInvocable( + [](dom::Value const& context) -> Expected { + return dom::JSON::stringify(context); + })); } //------------------------------------------------ @@ -151,20 +146,20 @@ callTemplate( { Config const& config = domCorpus.getCorpus().config; - js::Scope scope(ctx_); - auto Handlebars = scope.getGlobal("Handlebars"); auto layoutDir = files::appendPath(config->addonsDir, "generator", "asciidoc", "layouts"); auto pathName = files::appendPath(layoutDir, name); MRDOX_TRY(auto fileText, files::getFileText(pathName)); - dom::Object options; - options.set("noEscape", true); - options.set("allowProtoPropertiesByDefault", true); - // VFALCO This makes Proxy objects stop working - //options.set("allowProtoMethodsByDefault", true); - MRDOX_TRY(auto templateFn, Handlebars->callProp("compile", fileText, options)); - MRDOX_TRY(auto result, templateFn.call(context, options)); - return result.getString(); + HandlebarsOptions options; + options.noEscape = true; + + Expected exp = + hbs_.try_render(fileText, context, options); + if (!exp) + { + return Unexpected(Error(exp.error().what())); + } + return *exp; } Expected diff --git a/src/lib/-adoc/Builder.hpp b/src/lib/-adoc/Builder.hpp index 833ce5c16..1e7a0c7b8 100644 --- a/src/lib/-adoc/Builder.hpp +++ b/src/lib/-adoc/Builder.hpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -33,6 +34,7 @@ namespace adoc { class Builder { js::Context ctx_; + Handlebars hbs_; public: AdocCorpus const& domCorpus; diff --git a/src/lib/Support/JavaScript.cpp b/src/lib/Support/JavaScript.cpp index f625d6fec..789c69058 100644 --- a/src/lib/Support/JavaScript.cpp +++ b/src/lib/Support/JavaScript.cpp @@ -244,13 +244,26 @@ script( Access A(*this); auto failed = duk_peval_lstring( A, jsCode.data(), jsCode.size()); - if(failed) + if (failed) return dukM_popError(*this); MRDOX_ASSERT(duk_get_type(A, -1) == DUK_TYPE_UNDEFINED); duk_pop(A); // result return Error::success(); } +Expected +Scope:: +compile( + std::string_view jsCode) +{ + Access A(*this); + auto failed = duk_pcompile_lstring( + A, 0, jsCode.data(), jsCode.size()); + if (failed) + return Unexpected(dukM_popError(*this)); + return Access::construct(-1, *this); +} + Value Scope:: getGlobalObject() @@ -719,9 +732,10 @@ domValue_get( { Access A(scope); idx = duk_require_normalize_index(A, idx); - switch(duk_get_type(A, idx)) + switch (duk_get_type(A, idx)) { case DUK_TYPE_UNDEFINED: + return dom::Kind::Undefined; case DUK_TYPE_NULL: return nullptr; case DUK_TYPE_BOOLEAN: @@ -732,15 +746,39 @@ domValue_get( return dukM_get_string(A, idx); case DUK_TYPE_OBJECT: { + if (duk_is_array(A, idx)) + { + dom::Array res; + duk_size_t len = duk_get_length(A, idx); + for (duk_size_t i = 0; i < len; ++i) + { + duk_get_prop_index(A, idx, i); + res.emplace_back(domValue_get(scope, -1)); + duk_pop(A); + } + } + if (duk_is_function(A, idx)) + { + return dom::Kind::Undefined; + } + if (duk_is_object(A, idx)) + { + dom::Object res; + duk_enum(A, idx, DUK_ENUM_OWN_PROPERTIES_ONLY); + while (duk_next(A, -1, 1)) + { + std::string_view key = dukM_get_string(A, -2); + res.set(key, domValue_get(scope, -1)); + duk_pop(A); + } + return res; + } return nullptr; } - case DUK_TYPE_BUFFER: - case DUK_TYPE_POINTER: - case DUK_TYPE_LIGHTFUNC: default: - MRDOX_UNREACHABLE(); + return dom::Kind::Undefined; } - A.addref(scope); + return dom::Kind::Undefined; } static @@ -778,6 +816,7 @@ domValue_push( } } + //------------------------------------------------ Value:: @@ -869,7 +908,14 @@ type() const noexcept case DUK_TYPE_BOOLEAN: return Type::boolean; case DUK_TYPE_NUMBER: return Type::number; case DUK_TYPE_STRING: return Type::string; - case DUK_TYPE_OBJECT: return Type::object; + case DUK_TYPE_OBJECT: + { + if (duk_is_function(A, idx_)) + return Type::function; + if (duk_is_array(A, idx_)) + return Type::array; + return Type::object; + } case DUK_TYPE_NONE: default: // unknown type @@ -896,6 +942,13 @@ getString() const dukM_get_string(A, idx_)); } +dom::Value +js::Value:: +getDom() const +{ + return domValue_get(*scope_, idx_); +} + void Value:: setlog() @@ -922,7 +975,21 @@ callImpl( { Access A(*scope_); duk_dup(A, idx_); - for(auto const& arg : args) + for (auto const& arg : args) + domValue_push(A, arg); + auto result = duk_pcall(A, args.size()); + if(result == DUK_EXEC_ERROR) + return Unexpected(dukM_popError(*scope_)); + return A.construct(-1, *scope_); +} + +Expected +Value:: +callImpl(std::span args) const +{ + Access A(*scope_); + duk_dup(A, idx_); + for (auto const& arg : args) domValue_push(A, arg); auto result = duk_pcall(A, args.size()); if(result == DUK_EXEC_ERROR) @@ -953,6 +1020,36 @@ callPropImpl( return A.construct(-1, *scope_); } +void +Value:: +set( + std::string_view key, + Value value) const +{ + Access A(*scope_); + // Push the key and value onto the stack + duk_push_lstring(A, key.data(), key.size()); + duk_dup(A, value.idx_); + // Insert the key-value pair into the object + duk_put_prop(A, idx_); + // Clean up the stack + duk_pop_n(A, 2); // Remove the key and value from the stack +} + +MRDOX_DECL +Value +Value:: +get(std::string_view key) const +{ + Access A(*scope_); + // Push the key for the value we want to retrieve + duk_push_lstring(A, key.data(), key.size()); + // Get the value associated with the key + duk_get_prop(A, idx_); + // Return value or `undefined` + return A.construct(-1, *scope_); +} + } // js } // mrdox } // clang