Loading editor/src/webedit_api.cpp +34 −0 Original line number Diff line number Diff line Loading @@ -37,6 +37,7 @@ #include <cstdio> #include <cstdlib> #include <set> #include <map> #include <unordered_map> #include <unordered_set> #include <regex> Loading Loading @@ -1788,6 +1789,22 @@ void collectCanvasMeta(const blogi::webedit::EditPlugin *node, json_object *out) } } // Walks the document tree collecting {instance_id: custom_css_id} for every // element that has a non-empty getCssId() (see the css_id widget property). // Used by renderTree() below to fix up internal "#we-<uuid>" anchors (e.g. // Button's scroll-to-section href, built purely from the raw target instance // id with no knowledge of that target's own rendered output) so they resolve // to the human id the target widget actually rendered. void collectCssIdOverrides(const blogi::webedit::EditPlugin *node, std::map<std::string, std::string> &out) { while (node) { std::string cssId = node->getCssId(); if (!cssId.empty()) out[std::string(node->getInstanceId().c_str())] = cssId; if (node->getChildElement()) collectCssIdOverrides(node->getChildElement(), out); node = node->nextElement(); } } // Interactive canvas layer injected into the preview iframe: click-to-select, // click-to-edit inline text (elements marked with data-we-textprop by the widgets // that support it), and drag-to-reorder. Every widget's root node already carries Loading Loading @@ -6381,6 +6398,9 @@ std::string webedit::Api::renderTree(const blogi::webedit::EditPlugin *node, con const std::string &extraJs) { if (!node) return ""; std::map<std::string, std::string> cssIdOverrides; collectCssIdOverrides(node, cssIdOverrides); libhtmlpp::HtmlElement wrapper("div"); auto *el = node; while (el) { Loading Loading @@ -6416,6 +6436,20 @@ std::string webedit::Api::renderTree(const blogi::webedit::EditPlugin *node, con libhtmlpp::HtmlString hs; libhtmlpp::print(wrapper, hs, false); std::string html = hs.str(); // Rewrite "#we-<uuid>" anchors (Button's scroll-to-section href) to the // custom css_id of their target, if that target set one -- the target's // own Render() already swapped its "id" attribute to the same value, so // this keeps the two in sync without either widget needing a reference // to the other. for (const auto &kv : cssIdOverrides) { std::string needle = "#we-" + kv.first; std::string replacement = "#" + kv.second; size_t pos = 0; while ((pos = html.find(needle, pos)) != std::string::npos) { html.replace(pos, needle.size(), replacement); pos += replacement.size(); } } if (!extraCss.empty()) { html = "<style>" + escapeInlineStyle(extraCss) + "</style>" + html; } Loading editor/widgets/common/css_id.h 0 → 100644 +28 −0 Original line number Diff line number Diff line #pragma once #include <string> #include <cctype> namespace blogi { namespace webedit { // Sanitizes a user-supplied "custom CSS id" into a value that is safe to // use both as an HTML id attribute and as a "#id" CSS/URL fragment // selector. Keeps only ASCII letters, digits, '-' and '_'; everything // else is dropped. A result starting with a digit is prefixed with // "id-" since CSS identifiers (and thus querySelector('#...')) must not // start with a digit. inline std::string sanitizeCssId(const std::string &raw) { std::string out; out.reserve(raw.size()); for (unsigned char c : raw) { if (std::isalnum(c) || c == '-' || c == '_') out += static_cast<char>(c); } if (!out.empty() && std::isdigit(static_cast<unsigned char>(out[0]))) { out = "id-" + out; } return out; } } // namespace webedit } // namespace blogi editor/widgets/section/section.cpp +19 −2 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "plugin.h" #include "translations.h" #include "../common/css_id.h" extern "C" { namespace blogi::webedit { Loading @@ -45,6 +46,7 @@ extern "C" { std::string content; std::string backgroundColor = ""; std::string cssClass = ""; std::string cssId = ""; std::string borderRadius = ""; std::string borderStyle = ""; std::string opacity = ""; Loading Loading @@ -261,6 +263,7 @@ extern "C" { addField("background_gradient_angle", "Gradient Angle", "text", "e.g. 135deg", "desktop"); addField("background_gradient_custom", "Gradient Custom", "text", "e.g. linear-gradient(135deg, #ff0000, #0000ff)", "desktop"); addField("css_class", "CSS Class", "text", "e.g. hero-section"); addField("css_id", "CSS ID", "text", "e.g. pricing-section"); addSpacingBox("padding", "Padding", "desktop"); addSpacingBox("margin", "Margin", "desktop"); addField("max_width", "Max Width", "text", "e.g. 1200px", "desktop"); Loading Loading @@ -338,6 +341,7 @@ extern "C" { // Persist properties element->SetAttribute("background_color", backgroundColor.c_str()); element->SetAttribute("css_class", cssClass.c_str()); element->SetAttribute("css_id", cssId.c_str()); if (semanticTag != "section") { element->SetAttribute("semantic_tag", semanticTag.c_str()); } Loading Loading @@ -448,6 +452,8 @@ extern "C" { if (val) backgroundColor = val; val = xml_data->Attribute("css_class"); if (val) cssClass = val; val = xml_data->Attribute("css_id"); if (val) cssId = val; val = xml_data->Attribute("semantic_tag"); if (val) { static const std::vector<std::string> allowedTags = { Loading Loading @@ -617,6 +623,7 @@ extern "C" { json_object *obj = json_object_new_object(); json_object_object_add(obj, "background_color", json_object_new_string(backgroundColor.c_str())); json_object_object_add(obj, "css_class", json_object_new_string(cssClass.c_str())); json_object_object_add(obj, "css_id", json_object_new_string(cssId.c_str())); json_object_object_add(obj, "border_radius", json_object_new_string(borderRadius.c_str())); json_object_object_add(obj, "border_style", json_object_new_string(borderStyle.c_str())); json_object_object_add(obj, "opacity", json_object_new_string(opacity.c_str())); Loading Loading @@ -693,6 +700,10 @@ extern "C" { val = json_object_get_string(val_obj); if (val) cssClass = val; } if (json_object_object_get_ex(obj, "css_id", &val_obj)) { val = json_object_get_string(val_obj); if (val) cssId = val; } if (json_object_object_get_ex(obj, "border_radius", &val_obj)) { val = json_object_get_string(val_obj); if (val) borderRadius = val; Loading Loading @@ -937,8 +948,12 @@ extern "C" { // Unique CSS class for responsive targeting std::string responsiveClass = std::string("we-") + getInstanceId().c_str(); // Anchor ID so buttons can scroll to this section sect.setAttribute("id", responsiveClass.c_str()); // Anchor ID so buttons can scroll to this section -- a custom // css_id, when set, replaces the auto "we-<uuid>" id (see // renderTree(), which fixes up Button's scroll-to-section // anchor to follow this override). std::string customCssId = sanitizeCssId(cssId); sect.setAttribute("id", (customCssId.empty() ? responsiveClass : customCssId).c_str()); auto gradientCss = [](const std::string &type, const std::string &angle, const std::string &c1, const std::string &c2) -> std::string { if (type == "linear") { Loading Loading @@ -1178,6 +1193,8 @@ extern "C" { el.appendChild(sect); } std::string getCssId() const override { return sanitizeCssId(cssId); } // --- Element Management --- bool canHaveChildren() const override { return true; } Loading src/plugin.h +7 −0 Original line number Diff line number Diff line Loading @@ -427,6 +427,13 @@ namespace blogi { // Article, List, Table, FAQ, ...) override this. virtual std::optional<std::string> toLlmsBlock() const { return std::nullopt; } // The sanitized user-supplied "custom CSS id" (see the css_id // widget property), or "" when unset/unsupported. renderTree() // uses this to resolve internal "#we-<uuid>" anchors (e.g. // Button's scroll-to-section) to the human id actually rendered, // without any widget needing to know about any other widget. virtual std::string getCssId() const { return ""; } // Sets the UI language this instance should render property-panel // field labels in ("de"/"en"). Called before get_schema so // getSchemaJson() can emit already-localized labels directly — Loading Loading
editor/src/webedit_api.cpp +34 −0 Original line number Diff line number Diff line Loading @@ -37,6 +37,7 @@ #include <cstdio> #include <cstdlib> #include <set> #include <map> #include <unordered_map> #include <unordered_set> #include <regex> Loading Loading @@ -1788,6 +1789,22 @@ void collectCanvasMeta(const blogi::webedit::EditPlugin *node, json_object *out) } } // Walks the document tree collecting {instance_id: custom_css_id} for every // element that has a non-empty getCssId() (see the css_id widget property). // Used by renderTree() below to fix up internal "#we-<uuid>" anchors (e.g. // Button's scroll-to-section href, built purely from the raw target instance // id with no knowledge of that target's own rendered output) so they resolve // to the human id the target widget actually rendered. void collectCssIdOverrides(const blogi::webedit::EditPlugin *node, std::map<std::string, std::string> &out) { while (node) { std::string cssId = node->getCssId(); if (!cssId.empty()) out[std::string(node->getInstanceId().c_str())] = cssId; if (node->getChildElement()) collectCssIdOverrides(node->getChildElement(), out); node = node->nextElement(); } } // Interactive canvas layer injected into the preview iframe: click-to-select, // click-to-edit inline text (elements marked with data-we-textprop by the widgets // that support it), and drag-to-reorder. Every widget's root node already carries Loading Loading @@ -6381,6 +6398,9 @@ std::string webedit::Api::renderTree(const blogi::webedit::EditPlugin *node, con const std::string &extraJs) { if (!node) return ""; std::map<std::string, std::string> cssIdOverrides; collectCssIdOverrides(node, cssIdOverrides); libhtmlpp::HtmlElement wrapper("div"); auto *el = node; while (el) { Loading Loading @@ -6416,6 +6436,20 @@ std::string webedit::Api::renderTree(const blogi::webedit::EditPlugin *node, con libhtmlpp::HtmlString hs; libhtmlpp::print(wrapper, hs, false); std::string html = hs.str(); // Rewrite "#we-<uuid>" anchors (Button's scroll-to-section href) to the // custom css_id of their target, if that target set one -- the target's // own Render() already swapped its "id" attribute to the same value, so // this keeps the two in sync without either widget needing a reference // to the other. for (const auto &kv : cssIdOverrides) { std::string needle = "#we-" + kv.first; std::string replacement = "#" + kv.second; size_t pos = 0; while ((pos = html.find(needle, pos)) != std::string::npos) { html.replace(pos, needle.size(), replacement); pos += replacement.size(); } } if (!extraCss.empty()) { html = "<style>" + escapeInlineStyle(extraCss) + "</style>" + html; } Loading
editor/widgets/common/css_id.h 0 → 100644 +28 −0 Original line number Diff line number Diff line #pragma once #include <string> #include <cctype> namespace blogi { namespace webedit { // Sanitizes a user-supplied "custom CSS id" into a value that is safe to // use both as an HTML id attribute and as a "#id" CSS/URL fragment // selector. Keeps only ASCII letters, digits, '-' and '_'; everything // else is dropped. A result starting with a digit is prefixed with // "id-" since CSS identifiers (and thus querySelector('#...')) must not // start with a digit. inline std::string sanitizeCssId(const std::string &raw) { std::string out; out.reserve(raw.size()); for (unsigned char c : raw) { if (std::isalnum(c) || c == '-' || c == '_') out += static_cast<char>(c); } if (!out.empty() && std::isdigit(static_cast<unsigned char>(out[0]))) { out = "id-" + out; } return out; } } // namespace webedit } // namespace blogi
editor/widgets/section/section.cpp +19 −2 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "plugin.h" #include "translations.h" #include "../common/css_id.h" extern "C" { namespace blogi::webedit { Loading @@ -45,6 +46,7 @@ extern "C" { std::string content; std::string backgroundColor = ""; std::string cssClass = ""; std::string cssId = ""; std::string borderRadius = ""; std::string borderStyle = ""; std::string opacity = ""; Loading Loading @@ -261,6 +263,7 @@ extern "C" { addField("background_gradient_angle", "Gradient Angle", "text", "e.g. 135deg", "desktop"); addField("background_gradient_custom", "Gradient Custom", "text", "e.g. linear-gradient(135deg, #ff0000, #0000ff)", "desktop"); addField("css_class", "CSS Class", "text", "e.g. hero-section"); addField("css_id", "CSS ID", "text", "e.g. pricing-section"); addSpacingBox("padding", "Padding", "desktop"); addSpacingBox("margin", "Margin", "desktop"); addField("max_width", "Max Width", "text", "e.g. 1200px", "desktop"); Loading Loading @@ -338,6 +341,7 @@ extern "C" { // Persist properties element->SetAttribute("background_color", backgroundColor.c_str()); element->SetAttribute("css_class", cssClass.c_str()); element->SetAttribute("css_id", cssId.c_str()); if (semanticTag != "section") { element->SetAttribute("semantic_tag", semanticTag.c_str()); } Loading Loading @@ -448,6 +452,8 @@ extern "C" { if (val) backgroundColor = val; val = xml_data->Attribute("css_class"); if (val) cssClass = val; val = xml_data->Attribute("css_id"); if (val) cssId = val; val = xml_data->Attribute("semantic_tag"); if (val) { static const std::vector<std::string> allowedTags = { Loading Loading @@ -617,6 +623,7 @@ extern "C" { json_object *obj = json_object_new_object(); json_object_object_add(obj, "background_color", json_object_new_string(backgroundColor.c_str())); json_object_object_add(obj, "css_class", json_object_new_string(cssClass.c_str())); json_object_object_add(obj, "css_id", json_object_new_string(cssId.c_str())); json_object_object_add(obj, "border_radius", json_object_new_string(borderRadius.c_str())); json_object_object_add(obj, "border_style", json_object_new_string(borderStyle.c_str())); json_object_object_add(obj, "opacity", json_object_new_string(opacity.c_str())); Loading Loading @@ -693,6 +700,10 @@ extern "C" { val = json_object_get_string(val_obj); if (val) cssClass = val; } if (json_object_object_get_ex(obj, "css_id", &val_obj)) { val = json_object_get_string(val_obj); if (val) cssId = val; } if (json_object_object_get_ex(obj, "border_radius", &val_obj)) { val = json_object_get_string(val_obj); if (val) borderRadius = val; Loading Loading @@ -937,8 +948,12 @@ extern "C" { // Unique CSS class for responsive targeting std::string responsiveClass = std::string("we-") + getInstanceId().c_str(); // Anchor ID so buttons can scroll to this section sect.setAttribute("id", responsiveClass.c_str()); // Anchor ID so buttons can scroll to this section -- a custom // css_id, when set, replaces the auto "we-<uuid>" id (see // renderTree(), which fixes up Button's scroll-to-section // anchor to follow this override). std::string customCssId = sanitizeCssId(cssId); sect.setAttribute("id", (customCssId.empty() ? responsiveClass : customCssId).c_str()); auto gradientCss = [](const std::string &type, const std::string &angle, const std::string &c1, const std::string &c2) -> std::string { if (type == "linear") { Loading Loading @@ -1178,6 +1193,8 @@ extern "C" { el.appendChild(sect); } std::string getCssId() const override { return sanitizeCssId(cssId); } // --- Element Management --- bool canHaveChildren() const override { return true; } Loading
src/plugin.h +7 −0 Original line number Diff line number Diff line Loading @@ -427,6 +427,13 @@ namespace blogi { // Article, List, Table, FAQ, ...) override this. virtual std::optional<std::string> toLlmsBlock() const { return std::nullopt; } // The sanitized user-supplied "custom CSS id" (see the css_id // widget property), or "" when unset/unsupported. renderTree() // uses this to resolve internal "#we-<uuid>" anchors (e.g. // Button's scroll-to-section) to the human id actually rendered, // without any widget needing to know about any other widget. virtual std::string getCssId() const { return ""; } // Sets the UI language this instance should render property-panel // field labels in ("de"/"en"). Called before get_schema so // getSchemaJson() can emit already-localized labels directly — Loading