Commit d387ff6b authored by jan.koester's avatar jan.koester
Browse files

test

parent 6655d934
Loading
Loading
Loading
Loading
+1 −21
Original line number Diff line number Diff line
@@ -7,8 +7,6 @@ include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(NodeJS REQUIRED)

# When built standalone, add parent cmake module path
if(NOT TARGET blogidev)
    list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/../cmake)
@@ -43,31 +41,14 @@ add_executable(blogi-editor
    src/webedit_server.cpp
    src/webedit_api.cpp
    src/htmlimport.cpp
    widgets/nodewidget/nodewidget_adapter.cpp
    src/nodewidget_adapter_stub.cpp
)

find_path(LIBNODE_INCLUDE_DIR NAMES node.h PATH_SUFFIXES node include/node)
find_library(LIBNODE_LIBRARY NAMES node libnode)

if(NOT LIBNODE_INCLUDE_DIR OR NOT LIBNODE_LIBRARY)
    message(FATAL_ERROR "libnode is required but was not found. Install libnode headers/library and reconfigure.")
endif()

message(STATUS "libnode enabled: ${LIBNODE_LIBRARY} (${LIBNODE_INCLUDE_DIR})")
target_compile_definitions(blogi-editor PRIVATE BLOGI_HAVE_LIBNODE=1)
target_include_directories(blogi-editor PRIVATE ${LIBNODE_INCLUDE_DIR})

# Cache für Widgets setzen
set(BLOGI_LIBNODE_INCLUDE_DIR "${LIBNODE_INCLUDE_DIR}" CACHE INTERNAL "blogi libnode include dir")
set(BLOGI_LIBNODE_LIBRARY "${LIBNODE_LIBRARY}" CACHE INTERNAL "blogi libnode library")

if(TARGET blogidev)
    target_link_libraries(blogi-editor PRIVATE
        blogidev
        json-c::json-c
        tinyxml2::tinyxml2
        NodeJS::NodeJS
        dl
    )
    target_sources(blogi-editor PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/../src/plugin.cpp
@@ -75,7 +56,6 @@ if(TARGET blogidev)
    )
else()
    target_link_libraries(blogi-editor PRIVATE
        NodeJS::NodeJS
        httppp::httppp
        netplus::netplus
        htmlpp::htmlpp
+433 −0
Original line number Diff line number Diff line
#include "../widgets/nodewidget/nodewidget_adapter.h"

#include <cctype>
#include <cstring>
#include <fstream>
#include <iostream>
#include <sstream>
#include <stdexcept>

namespace blogi::webedit {

NodeWidgetAdapter::NodeWidgetAdapter(const std::string &defaultName,
                                     const std::string &defaultAuthor,
                                     const std::string &defaultVersion,
                                     const std::string &typeId,
                                     const std::string &defaultScriptPath,
                                     bool defaultCanHaveChildren)
    : scriptPath(defaultScriptPath),
      defaultName(defaultName),
      defaultAuthor(defaultAuthor),
      defaultVersion(defaultVersion),
      canHaveChildrenFlag(defaultCanHaveChildren),
      name(defaultName),
      author(defaultAuthor),
      version(defaultVersion) {
    if (!typeId.empty()) {
        try { Type = typeId.c_str(); } catch (...) {}
    }
}

const std::string NodeWidgetAdapter::getName() const { return name; }
const std::string NodeWidgetAdapter::getAuthor() const { return author; }
const std::string NodeWidgetAdapter::getVersion() const { return version; }

void NodeWidgetAdapter::logError(const std::string &message) {
    std::cerr << "NodeWidgetAdapter Error (" << getInstanceId().c_str() << "): "
              << message << std::endl;
}

void NodeWidgetAdapter::Settings(libhttppp::HttpRequest &, libhtmlpp::HtmlElement &settings) {
    appendDefaultSettings(settings);
}

void NodeWidgetAdapter::JsonApi(json_object *request, json_object *response) {
    if (!request || !response) return;

    json_object *actionObj = nullptr;
    const char *action = nullptr;
    if (json_object_object_get_ex(request, "action", &actionObj))
        action = json_object_get_string(actionObj);

    if (action && std::strcmp(action, "modify_bulk") == 0) {
        json_object *propsObj = nullptr;
        if (json_object_object_get_ex(request, "properties", &propsObj)) {
            const char *jsonStr = json_object_to_json_string_ext(propsObj, JSON_C_TO_STRING_NOSLASHESCAPE);
            if (jsonStr) fromPropertiesJson(jsonStr);
        }
    } else if (action && std::strcmp(action, "read") == 0) {
        json_object *data = json_object_new_object();
        json_object_object_add(data, "properties", json_object_new_string(toPropertiesJson().c_str()));
        json_object_object_add(response, "data", data);
    } else if (action && std::strcmp(action, "get_schema") == 0) {
        json_object_object_add(response, "schema", getSchemaJson());
    }

    json_object_object_add(response, "status", json_object_new_string("success"));
    json_object_object_add(response, "plugin_type", json_object_new_string(getName().c_str()));
}

tinyxml2::XMLElement *NodeWidgetAdapter::toXmlObject(tinyxml2::XMLDocument *doc) {
    tinyxml2::XMLElement *element = doc->NewElement(getName().c_str());
    element->SetAttribute("instance_id", getInstanceId().c_str());
    element->SetAttribute("type_id", getTypeId().c_str());

    tinyxml2::XMLElement *props = doc->NewElement("PropsJson");
    const std::string serializedProps = toPropertiesJson();
    props->SetText(serializedProps.c_str());
    element->InsertEndChild(props);

    if (!fallbackHtml.empty()) {
        tinyxml2::XMLElement *fallback = doc->NewElement("FallbackHtml");
        fallback->SetText(fallbackHtml.c_str());
        element->InsertEndChild(fallback);
    }

    const EditPlugin *cur = child;
    while (cur) {
        element->InsertEndChild(const_cast<EditPlugin *>(cur)->toXmlObject(doc));
        cur = cur->nextElement();
    }

    return element;
}

void NodeWidgetAdapter::fromXmlObject(tinyxml2::XMLElement *xml_data) {
    const char *val = nullptr;

    val = xml_data->Attribute("type_id");
    if (val && val[0] != '\0') {
        try { Type = val; } catch (...) {}
    }

    tinyxml2::XMLElement *props = xml_data->FirstChildElement("PropsJson");
    if (props && props->GetText()) {
        propsJson = props->GetText();
        fromPropertiesJson(propsJson);
    } else {
        val = xml_data->Attribute("script_path"); if (val) scriptPath = val;
        val = xml_data->Attribute("wrapper_tag"); if (val) wrapperTag = val;
        val = xml_data->Attribute("can_have_children"); if (val) canHaveChildrenFlag = (std::strcmp(val, "true") == 0);
        val = xml_data->Attribute("name"); if (val) name = val;
        val = xml_data->Attribute("author"); if (val) author = val;
        val = xml_data->Attribute("version"); if (val) version = val;
    }

    tinyxml2::XMLElement *fallback = xml_data->FirstChildElement("FallbackHtml");
    if (fallback && fallback->GetText()) fallbackHtml = fallback->GetText();

    metadataLoaded = false;
}

void NodeWidgetAdapter::Render(libhtmlpp::HtmlElement &el) const {
    std::string html = fallbackHtml;

    libhtmlpp::HtmlElement wrapper(safeWrapperTag().c_str());
    wrapper.setAttribute("class", "we-node-widget");
    wrapper.setAttribute("data-instance-id", getInstanceId().c_str());

    if (!html.empty()) {
        try {
            libhtmlpp::HtmlString parsed("<div>" + html + "</div>");
            wrapper.appendChild(parsed.parse());
        } catch (...) {
            wrapper.appendChild(libhtmlpp::TextElement(html.c_str()));
        }
    }

    const EditPlugin *cur = child;
    while (cur) {
        cur->Render(wrapper);
        cur = cur->nextElement();
    }

    el.appendChild(wrapper);
}

bool NodeWidgetAdapter::canHaveChildren() const { return canHaveChildrenFlag; }

bool NodeWidgetAdapter::haveChilds() {
    return child != nullptr;
}

void NodeWidgetAdapter::setChildElement(const EditPlugin &el) {
    if (!canHaveChildren()) {
        logError("Widget cannot have children.");
        return;
    }
    child = const_cast<EditPlugin *>(&el);
}

void NodeWidgetAdapter::unsetChildElement(const EditPlugin &el) {
    if (child == &el) child = const_cast<EditPlugin *>(el.nextElement());
}

void NodeWidgetAdapter::addNextElement(const EditPlugin &el) {
    if (!canHaveChildren()) {
        logError("Widget does not support addNextElement.");
        return;
    }

    EditPlugin *newEl = const_cast<EditPlugin *>(&el);
    if (!child) {
        setChildElement(el);
        return;
    }

    EditPlugin *cur = child;
    while (cur->nextElement() != nullptr)
        cur = const_cast<EditPlugin *>(cur->nextElement());

    cur->setNextEl(newEl);
}

void NodeWidgetAdapter::remNextElement(const EditPlugin &el) {
    if (!child) return;

    if (child == &el) {
        unsetChildElement(el);
        return;
    }

    EditPlugin *cur = child;
    while (cur->nextElement() != nullptr) {
        if (cur->nextElement() == &el) {
            cur->setNextEl(const_cast<EditPlugin *>(el.nextElement()));
            return;
        }
        cur = const_cast<EditPlugin *>(cur->nextElement());
    }
}

json_object *NodeWidgetAdapter::getSchemaJson() {
    json_object *arr = json_object_new_array();

    auto addField = [&](const char *key, const char *label, const char *type,
                        const char *placeholder = nullptr, const char *target = "all") {
        json_object *f = json_object_new_object();
        json_object_object_add(f, "key", json_object_new_string(key));
        json_object_object_add(f, "label", json_object_new_string(label));
        json_object_object_add(f, "type", json_object_new_string(type));
        json_object_object_add(f, "target", json_object_new_string(target));
        if (placeholder)
            json_object_object_add(f, "placeholder", json_object_new_string(placeholder));
        json_object_array_add(arr, f);
    };

    addField("script_path", "Node Widget Script", "text", "e.g. /opt/blogi/widgets/hero.js");
    addField("type_id", "Type ID", "text", "UUID for this widget type");
    addField("props_json", "Properties JSON", "textarea", "{\"title\":\"Hello\"}");
    addField("fallback_html", "Fallback HTML", "textarea", "Shown if Node render fails");
    addField("wrapper_tag", "Wrapper Tag", "text", "div");
    addField("can_have_children", "Can Have Children", "checkbox");

    return arr;
}

std::string NodeWidgetAdapter::toPropertiesJson() const {
    json_object *obj = parsePropsJsonObject();
    json_object_object_add(obj, "script_path", json_object_new_string(scriptPath.c_str()));
    json_object_object_add(obj, "type_id", json_object_new_string(getTypeId().c_str()));
    json_object_object_add(obj, "props_json", json_object_new_string(propsJson.c_str()));
    json_object_object_add(obj, "fallback_html", json_object_new_string(fallbackHtml.c_str()));
    json_object_object_add(obj, "wrapper_tag", json_object_new_string(wrapperTag.c_str()));
    json_object_object_add(obj, "can_have_children", json_object_new_boolean(canHaveChildrenFlag));
    json_object_object_add(obj, "name", json_object_new_string(name.c_str()));
    json_object_object_add(obj, "author", json_object_new_string(author.c_str()));
    json_object_object_add(obj, "version", json_object_new_string(version.c_str()));
    const char *out = json_object_to_json_string_ext(obj, JSON_C_TO_STRING_NOSLASHESCAPE);
    std::string ret = out ? out : "{}";
    json_object_put(obj);
    return ret;
}

void NodeWidgetAdapter::fromPropertiesJson(const std::string &json) {
    json_object *obj = json_tokener_parse(json.c_str());
    if (!obj) return;

    json_object *customObj = json_object_new_object();
    json_object *valObj = nullptr;
    const char *val = nullptr;

    if (json_object_object_get_ex(obj, "props_json", &valObj) && json_object_is_type(valObj, json_type_string)) {
        const char *nested = json_object_get_string(valObj);
        if (nested && nested[0] != '\0') {
            json_object *nestedObj = json_tokener_parse(nested);
            if (nestedObj && json_object_is_type(nestedObj, json_type_object)) {
                mergeObjectInto(customObj, nestedObj);
            }
            if (nestedObj) json_object_put(nestedObj);
        }
    }

    auto isBaseKey = [](const char *key) {
        return std::strcmp(key, "script_path") == 0
            || std::strcmp(key, "type_id") == 0
            || std::strcmp(key, "props_json") == 0
            || std::strcmp(key, "fallback_html") == 0
            || std::strcmp(key, "wrapper_tag") == 0
            || std::strcmp(key, "can_have_children") == 0
            || std::strcmp(key, "name") == 0
            || std::strcmp(key, "author") == 0
            || std::strcmp(key, "version") == 0;
    };

    json_object_object_foreach(obj, key, value) {
        if (isBaseKey(key)) continue;
        json_object_object_add(customObj, key, json_object_get(value));
    }

    if (json_object_object_get_ex(obj, "script_path", &valObj)) {
        val = json_object_get_string(valObj);
        if (val) scriptPath = val;
    }
    if (json_object_object_get_ex(obj, "type_id", &valObj)) {
        val = json_object_get_string(valObj);
        if (val && val[0] != '\0') {
            try { Type = val; } catch (...) {}
        }
    }
    if (json_object_object_get_ex(obj, "props_json", &valObj)) {
        val = json_object_get_string(valObj);
        if (val) propsJson = val;
    }
    if (json_object_object_get_ex(obj, "fallback_html", &valObj)) {
        val = json_object_get_string(valObj);
        if (val) fallbackHtml = val;
    }
    if (json_object_object_get_ex(obj, "wrapper_tag", &valObj)) {
        val = json_object_get_string(valObj);
        if (val) wrapperTag = val;
    }
    if (json_object_object_get_ex(obj, "can_have_children", &valObj)) {
        if (json_object_is_type(valObj, json_type_boolean) || json_object_is_type(valObj, json_type_int)) {
            canHaveChildrenFlag = json_object_get_boolean(valObj);
        } else if (json_object_is_type(valObj, json_type_string)) {
            const char *b = json_object_get_string(valObj);
            canHaveChildrenFlag = (b && (std::strcmp(b, "true") == 0 || std::strcmp(b, "1") == 0));
        }
    }

    if (json_object_object_get_ex(obj, "name", &valObj)) {
        val = json_object_get_string(valObj);
        if (val && val[0] != '\0') name = val;
    }
    if (json_object_object_get_ex(obj, "author", &valObj)) {
        val = json_object_get_string(valObj);
        if (val && val[0] != '\0') author = val;
    }
    if (json_object_object_get_ex(obj, "version", &valObj)) {
        val = json_object_get_string(valObj);
        if (val && val[0] != '\0') version = val;
    }

    const char *customStr = json_object_to_json_string_ext(customObj, JSON_C_TO_STRING_NOSLASHESCAPE);
    propsJson = customStr ? customStr : "{}";

    json_object_put(obj);
    json_object_put(customObj);
}

void NodeWidgetAdapter::ensureMetadataLoaded() const {}

bool NodeWidgetAdapter::renderNodeSettings(libhtmlpp::HtmlElement &) const {
    return false;
}

void NodeWidgetAdapter::appendDefaultSettings(libhtmlpp::HtmlElement &settings) const {
    libhtmlpp::HtmlElement title("h3");
    title.appendChild(libhtmlpp::TextElement((getName() + " Settings").c_str()));
    settings.appendChild(title);

    libhtmlpp::HtmlElement scriptLabel("label");
    scriptLabel.appendChild(libhtmlpp::TextElement("Script Path: "));
    settings.appendChild(scriptLabel);

    libhtmlpp::HtmlElement scriptInput("input");
    scriptInput.setAttribute("type", "text");
    scriptInput.setAttribute("name", "script_path");
    scriptInput.setAttribute("value", scriptPath.c_str());
    settings.appendChild(scriptInput);

    libhtmlpp::HtmlElement propsLabel("label");
    propsLabel.appendChild(libhtmlpp::TextElement("Properties JSON: "));
    settings.appendChild(propsLabel);

    libhtmlpp::HtmlElement propsArea("textarea");
    propsArea.setAttribute("name", "props_json");
    propsArea.setAttribute("rows", "8");
    propsArea.setAttribute("cols", "80");
    propsArea.appendChild(libhtmlpp::TextElement(propsJson.c_str()));
    settings.appendChild(propsArea);

    libhtmlpp::HtmlElement fallbackLabel("label");
    fallbackLabel.appendChild(libhtmlpp::TextElement("Fallback HTML (on Node errors): "));
    settings.appendChild(fallbackLabel);

    libhtmlpp::HtmlElement fallbackArea("textarea");
    fallbackArea.setAttribute("name", "fallback_html");
    fallbackArea.setAttribute("rows", "6");
    fallbackArea.setAttribute("cols", "80");
    fallbackArea.appendChild(libhtmlpp::TextElement(fallbackHtml.c_str()));
    settings.appendChild(fallbackArea);
}

void NodeWidgetAdapter::notifyChildHook(const char *, const EditPlugin &) {}

std::string NodeWidgetAdapter::readFile(const std::string &path) {
    std::ifstream in(path);
    if (!in.is_open()) throw std::runtime_error("Cannot read file: " + path);
    std::ostringstream ss;
    ss << in.rdbuf();
    return ss.str();
}

void NodeWidgetAdapter::writeFile(const std::string &path, const std::string &data) {
    std::ofstream out(path, std::ios::trunc);
    if (!out.is_open()) throw std::runtime_error("Cannot write file: " + path);
    out << data;
}

std::string NodeWidgetAdapter::makeTempPath(const std::string &name) {
    return "/tmp/blogi_stub_" + name;
}

void NodeWidgetAdapter::mergeObjectInto(json_object *dst, json_object *src) {
    if (!dst || !src) return;
    json_object_object_foreach(src, key, val) {
        json_object_get(val);
        json_object_object_add(dst, key, val);
    }
}

json_object *NodeWidgetAdapter::parsePropsJsonObject() const {
    json_object *obj = json_tokener_parse(propsJson.c_str());
    if (!obj || !json_object_is_type(obj, json_type_object)) {
        if (obj) json_object_put(obj);
        return json_object_new_object();
    }
    return obj;
}

std::string NodeWidgetAdapter::runtimePluginName() const {
    if (!defaultName.empty()) return defaultName;
    if (!name.empty()) return name;
    return "NodeWidget";
}

NodeWidgetAdapter::NodeCallResult NodeWidgetAdapter::callHook(const char *, json_object *) const {
    NodeCallResult out;
    out.ok = false;
    out.missingHook = true;
    return out;
}

std::string NodeWidgetAdapter::safeWrapperTag() const {
    if (wrapperTag.empty()) return "div";
    for (char c : wrapperTag) {
        if (!std::isalnum(static_cast<unsigned char>(c))) return "div";
    }
    return wrapperTag;
}

}
+8 −69
Original line number Diff line number Diff line
@@ -49,7 +49,6 @@

#include "webedit_api.h"
#include "htmlimport.h"
#include "../widgets/nodewidget/node_embedded.h"

#include "../widgets/nodewidget/nodewidget_adapter.h"

@@ -64,77 +63,17 @@ struct NodeWidgetDescriptor {
};

std::string getEnvOrDefault(const char *key, const char *fallback) {
    return blogi::webedit::embedded::getEnvOrDefault(key, fallback);
    const char *v = std::getenv(key);
    if (v && v[0] != '\0') {
        return std::string(v);
    }
    return std::string(fallback);
}

bool queryNodeWidgetDescriptor(const std::string &scriptPath, NodeWidgetDescriptor &desc) {
    bool ok = false;
    json_object *req = json_object_new_object();
    json_object_object_add(req, "script_path", json_object_new_string(scriptPath.c_str()));
    json_object_object_add(req, "instance_id", json_object_new_string("catalog"));
    json_object_object_add(req, "plugin_type", json_object_new_string("NodeWidget"));
    json_object_object_add(req, "type_id", json_object_new_string(""));
    json_object_object_add(req, "hook", json_object_new_string("metadata"));
    json_object_object_add(req, "properties", json_object_new_object());

    const char *reqJson = json_object_to_json_string_ext(req, JSON_C_TO_STRING_NOSLASHESCAPE);
    std::string reqJsonStr = reqJson ? reqJson : "{}";
    json_object_put(req);

    std::string raw;
    try {
        (void)blogi::webedit::embedded::runNodeRunnerJson(reqJsonStr, raw);
    } catch (const std::exception &e) {
        std::cerr << "[webedit] node metadata failed for " << scriptPath
                  << ": " << e.what() << std::endl;
    (void)scriptPath;
    (void)desc;
    return false;
    } catch (...) {
        std::cerr << "[webedit] node metadata failed for " << scriptPath
                  << ": unknown exception" << std::endl;
        return false;
    }

    json_object *res = raw.empty() ? nullptr : json_tokener_parse(raw.c_str());
    if (res && json_object_is_type(res, json_type_object)) {
        json_object *statusObj = nullptr;
        const char *status = nullptr;
        if (json_object_object_get_ex(res, "status", &statusObj)) {
            status = json_object_get_string(statusObj);
        }

        if (status && std::strcmp(status, "success") == 0) {
            json_object *resultObj = nullptr;
            if (json_object_object_get_ex(res, "result", &resultObj) && json_object_is_type(resultObj, json_type_object)) {
                json_object *obj = nullptr;
                if (json_object_object_get_ex(resultObj, "name", &obj)) {
                    const char *v = json_object_get_string(obj);
                    if (v) desc.name = v;
                }
                if (json_object_object_get_ex(resultObj, "type_id", &obj)) {
                    const char *v = json_object_get_string(obj);
                    if (v) desc.typeId = v;
                }
                if (json_object_object_get_ex(resultObj, "author", &obj)) {
                    const char *v = json_object_get_string(obj);
                    if (v) desc.author = v;
                }
                if (json_object_object_get_ex(resultObj, "version", &obj)) {
                    const char *v = json_object_get_string(obj);
                    if (v) desc.version = v;
                }
                if (json_object_object_get_ex(resultObj, "can_have_children", &obj)) {
                    if (json_object_is_type(obj, json_type_boolean) || json_object_is_type(obj, json_type_int)) {
                        desc.canHaveChildren = json_object_get_boolean(obj);
                    }
                }

                ok = !desc.name.empty() && !desc.typeId.empty();
            }
        }
    }

    if (res) json_object_put(res);
    return ok;
}

const std::vector<NodeWidgetDescriptor> &nodeWidgetCatalog() {
+13 −4
Original line number Diff line number Diff line
find_package(NodeJS REQUIRED)

find_path(LIBNODE_INCLUDE_DIR NAMES node.h PATH_SUFFIXES node include/node)
find_library(LIBNODE_LIBRARY NAMES node libnode)

if(NOT LIBNODE_INCLUDE_DIR OR NOT LIBNODE_LIBRARY)
    message(FATAL_ERROR "libnode is required but was not found for nodewidget")
endif()

add_library(nodewidget SHARED nodewidget.cpp nodewidget_adapter.cpp)

target_include_directories(nodewidget PRIVATE ${BLOGI_LIBNODE_INCLUDE_DIR})
target_include_directories(nodewidget PRIVATE ${LIBNODE_INCLUDE_DIR})
target_compile_definitions(nodewidget PRIVATE BLOGI_HAVE_LIBNODE=1)

if(TARGET blogidev)
    add_dependencies(nodewidget blogidev)
    if(${CMAKE_HOST_SYSTEM_NAME} STREQUAL "Windows")
        target_link_libraries(nodewidget PUBLIC blogidev kernel32.lib NodeJS::NodeJS uuidp::uuidp htmlpp::htmlpp json-c::json-c tinyxml2::tinyxml2)
        target_link_libraries(nodewidget PUBLIC blogidev kernel32.lib NodeJS::NodeJS ${LIBNODE_LIBRARY} uuidp::uuidp htmlpp::htmlpp json-c::json-c tinyxml2::tinyxml2)
    else()
        target_link_libraries(nodewidget PUBLIC blogidev dl NodeJS::NodeJS uuidp::uuidp htmlpp::htmlpp json-c::json-c tinyxml2::tinyxml2)
        target_link_libraries(nodewidget PUBLIC blogidev dl NodeJS::NodeJS ${LIBNODE_LIBRARY} uuidp::uuidp htmlpp::htmlpp json-c::json-c tinyxml2::tinyxml2)
    endif()
else()
    target_link_libraries(nodewidget PUBLIC editor_plugin_base NodeJS::NodeJS uuidp::uuidp htmlpp::htmlpp json-c::json-c tinyxml2::tinyxml2 dl)
    target_link_libraries(nodewidget PUBLIC editor_plugin_base NodeJS::NodeJS ${LIBNODE_LIBRARY} uuidp::uuidp htmlpp::htmlpp json-c::json-c tinyxml2::tinyxml2 dl)
endif()

install(TARGETS nodewidget DESTINATION lib/blogi/plugins/webedit)