Loading editor/CMakeLists.txt +1 −21 Original line number Diff line number Diff line Loading @@ -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) Loading Loading @@ -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 Loading @@ -75,7 +56,6 @@ if(TARGET blogidev) ) else() target_link_libraries(blogi-editor PRIVATE NodeJS::NodeJS httppp::httppp netplus::netplus htmlpp::htmlpp Loading editor/src/nodewidget_adapter_stub.cpp 0 → 100644 +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; } } editor/src/webedit_api.cpp +8 −69 Original line number Diff line number Diff line Loading @@ -49,7 +49,6 @@ #include "webedit_api.h" #include "htmlimport.h" #include "../widgets/nodewidget/node_embedded.h" #include "../widgets/nodewidget/nodewidget_adapter.h" Loading @@ -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() { Loading editor/widgets/nodewidget/CMakeLists.txt +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) Loading
editor/CMakeLists.txt +1 −21 Original line number Diff line number Diff line Loading @@ -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) Loading Loading @@ -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 Loading @@ -75,7 +56,6 @@ if(TARGET blogidev) ) else() target_link_libraries(blogi-editor PRIVATE NodeJS::NodeJS httppp::httppp netplus::netplus htmlpp::htmlpp Loading
editor/src/nodewidget_adapter_stub.cpp 0 → 100644 +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; } }
editor/src/webedit_api.cpp +8 −69 Original line number Diff line number Diff line Loading @@ -49,7 +49,6 @@ #include "webedit_api.h" #include "htmlimport.h" #include "../widgets/nodewidget/node_embedded.h" #include "../widgets/nodewidget/nodewidget_adapter.h" Loading @@ -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() { Loading
editor/widgets/nodewidget/CMakeLists.txt +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)