Loading editor/widgets/list/list.cpp +68 −5 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 "llms_text.h" #include "../common/svg_markup.h" extern "C" { namespace blogi::webedit { Loading @@ -54,6 +55,9 @@ extern "C" { std::string fontColor = "var(--blogi-text)"; std::string lineHeight = "1.6"; std::string listStyleType = ""; // disc, circle, square, decimal, etc. (empty = browser default) // Resolved when list_style_type == "svg" (picked via the Icon Library) std::string svgMarkerIconId = ""; std::string svgMarkerSvg = ""; std::string direction = "vertical"; // "vertical" or "horizontal" std::string gap = ""; std::string alignItems = ""; Loading Loading @@ -205,7 +209,8 @@ extern "C" { 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 *options = nullptr, const char *level = "expert") { json_object *options = nullptr, const char *level = "expert", json_object *visibleWhen = nullptr) { 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)); Loading @@ -213,6 +218,7 @@ extern "C" { if (placeholder) json_object_object_add(f, "placeholder", json_object_new_string(placeholder)); json_object_object_add(f, "target", json_object_new_string(target)); if (options) json_object_object_add(f, "options", options); if (visibleWhen) json_object_object_add(f, "visibleWhen", visibleWhen); json_object_object_add(f, "level", json_object_new_string(level)); json_object_array_add(arr, f); }; Loading @@ -221,6 +227,12 @@ extern "C" { for (auto v : vals) json_object_array_add(a, json_object_new_string(v)); return a; }; auto mkVisible = [](const char *key, const char *value) { json_object *v = json_object_new_object(); json_object_object_add(v, "key", json_object_new_string(key)); json_object_object_add(v, "value", json_object_new_string(value)); return v; }; // Renders as four linked top/right/bottom/left inputs with a lock // toggle in the properties panel (see properties.js _renderSpacingBox) // instead of a single raw CSS shorthand string. Loading @@ -244,7 +256,16 @@ extern "C" { mkOpts({"ul", "ol"}), "simple"); addField("list_style_type", "List Style", "select", nullptr, "all", mkOpts({"", "disc", "circle", "square", "decimal", "decimal-leading-zero", "lower-alpha", "upper-alpha", "lower-roman", "upper-roman", "none", "emoji_circle"}), "simple"); "lower-alpha", "upper-alpha", "lower-roman", "upper-roman", "none", "emoji_circle", "arrow", "svg"}), "simple"); addField("svg_marker_icon_id", "Marker Icon (from Library)", "icon_browse", nullptr, "all", nullptr, "simple", mkVisible("list_style_type", "svg")); { // Tells the icon_browse picker (properties.js) which sibling field // holds the resolved SVG source — Render() only ever reads svgMarkerSvg. json_object *f = json_object_array_get_idx(arr, json_object_array_length(arr) - 1); json_object_object_add(f, "svgTargetKey", json_object_new_string("svg_marker_svg")); } addField("svg_marker_svg", "", "hidden"); addField("direction", "Direction", "select", nullptr, "desktop", mkOpts({"vertical", "horizontal"})); addField("gap", "Gap", "text", "8px"); Loading Loading @@ -301,6 +322,7 @@ extern "C" { element->SetAttribute("type_id", getTypeId().c_str()); element->SetAttribute("list_type", listType.c_str()); element->SetAttribute("list_style_type", listStyleType.c_str()); element->SetAttribute("svg_marker_icon_id", svgMarkerIconId.c_str()); element->SetAttribute("direction", direction.c_str()); element->SetAttribute("gap", gap.c_str()); element->SetAttribute("align_items", alignItems.c_str()); Loading Loading @@ -342,6 +364,12 @@ extern "C" { if (!dataAttrs.empty()) element->SetAttribute("data_attrs", dataAttrs.c_str()); if (!svgMarkerSvg.empty()) { tinyxml2::XMLElement* svgEl = doc->NewElement("SvgMarkerSvg"); svgEl->SetText(svgMarkerSvg.c_str()); element->InsertEndChild(svgEl); } // Store items as child <Item> elements for (const auto &item : items) { tinyxml2::XMLElement *li = doc->NewElement("Item"); Loading @@ -357,6 +385,7 @@ extern "C" { val = xml_data->Attribute("list_type"); if (val) listType = val; val = xml_data->Attribute("list_style_type"); if (val) listStyleType = val; val = xml_data->Attribute("svg_marker_icon_id"); if (val) svgMarkerIconId = val; val = xml_data->Attribute("direction"); if (val) direction = val; val = xml_data->Attribute("gap"); if (val) gap = val; val = xml_data->Attribute("align_items"); if (val) alignItems = val; Loading Loading @@ -425,6 +454,12 @@ extern "C" { val = xml_data->Attribute("data_attrs"); if (val) dataAttrs = val; tinyxml2::XMLElement* svgEl = xml_data->FirstChildElement("SvgMarkerSvg"); if (svgEl) { const char* svgVal = svgEl->GetText(); if (svgVal) svgMarkerSvg = svgVal; } // Read items from child <Item> elements items.clear(); for (tinyxml2::XMLElement *li = xml_data->FirstChildElement("Item"); li; li = li->NextSiblingElement("Item")) { Loading @@ -450,6 +485,8 @@ extern "C" { json_object *obj = json_object_new_object(); json_object_object_add(obj, "list_type", json_object_new_string(listType.c_str())); json_object_object_add(obj, "list_style_type", json_object_new_string(listStyleType.c_str())); json_object_object_add(obj, "svg_marker_icon_id", json_object_new_string(svgMarkerIconId.c_str())); json_object_object_add(obj, "svg_marker_svg", json_object_new_string(svgMarkerSvg.c_str())); json_object_object_add(obj, "direction", json_object_new_string(direction.c_str())); json_object_object_add(obj, "gap", json_object_new_string(gap.c_str())); json_object_object_add(obj, "align_items", json_object_new_string(alignItems.c_str())); Loading Loading @@ -507,6 +544,12 @@ extern "C" { if (json_object_object_get_ex(obj, "list_style_type", &val_obj)) { val = json_object_get_string(val_obj); if (val) listStyleType = val; } if (json_object_object_get_ex(obj, "svg_marker_icon_id", &val_obj)) { val = json_object_get_string(val_obj); if (val) svgMarkerIconId = val; } if (json_object_object_get_ex(obj, "svg_marker_svg", &val_obj)) { val = json_object_get_string(val_obj); if (val) svgMarkerSvg = val; } if (json_object_object_get_ex(obj, "direction", &val_obj)) { val = json_object_get_string(val_obj); if (val && val[0]) direction = val; } Loading Loading @@ -652,7 +695,9 @@ extern "C" { if (!fontColor.empty()) style += " color: " + fontColor + ";"; if (!lineHeight.empty()) style += " line-height: " + lineHeight + ";"; bool useEmojiMarker = (listStyleType == "emoji_circle"); if (useEmojiMarker) style += " list-style-type: none;"; bool useArrowMarker = (listStyleType == "arrow"); bool useSvgMarker = (listStyleType == "svg" && !svgMarkerSvg.empty()); if (useEmojiMarker || useArrowMarker || useSvgMarker) style += " list-style-type: none;"; else if (!listStyleType.empty()) style += " list-style-type: " + listStyleType + ";"; std::string paddingShorthand = joinSpacingShorthand(paddingTop, paddingRight, paddingBottom, paddingLeft); if (!paddingShorthand.empty()) style += " padding: " + paddingShorthand + ";"; Loading @@ -668,11 +713,29 @@ extern "C" { for (const auto &item : items) { libhtmlpp::HtmlElement li("li"); if (useEmojiMarker) { if (useEmojiMarker || useArrowMarker || useSvgMarker) { li.setAttribute("style", "display: flex; align-items: center; gap: 0.5em;"); libhtmlpp::HtmlElement marker("span"); marker.setAttribute("style", "flex: none;"); marker.appendChild(libhtmlpp::TextElement("❗")); if (useSvgMarker) { // Re-wrap the picked Icon Library SVG at a fixed "1em" // slot size, following currentColor so the marker // inherits the list's font_color (mirrors Textbox's // custom-icon Render()). std::string viewBox, inner; svgmarkup::parse(svgMarkerSvg, viewBox, inner); std::string vb = viewBox.empty() ? "0 0 24 24" : viewBox; std::string svgHtml = "<svg width=\"1em\" height=\"1em\" viewBox=\"" + vb + "\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">" + inner + "</svg>"; try { libhtmlpp::HtmlString svgBuf(svgHtml); libhtmlpp::Element &parsedSvg = svgBuf.parse(); marker.appendChild(parsedSvg); } catch (...) {} } else { marker.appendChild(libhtmlpp::TextElement(useArrowMarker ? "➤" : "❗")); } li.appendChild(marker); } if (!item.empty()) { Loading Loading
editor/widgets/list/list.cpp +68 −5 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 "llms_text.h" #include "../common/svg_markup.h" extern "C" { namespace blogi::webedit { Loading @@ -54,6 +55,9 @@ extern "C" { std::string fontColor = "var(--blogi-text)"; std::string lineHeight = "1.6"; std::string listStyleType = ""; // disc, circle, square, decimal, etc. (empty = browser default) // Resolved when list_style_type == "svg" (picked via the Icon Library) std::string svgMarkerIconId = ""; std::string svgMarkerSvg = ""; std::string direction = "vertical"; // "vertical" or "horizontal" std::string gap = ""; std::string alignItems = ""; Loading Loading @@ -205,7 +209,8 @@ extern "C" { 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 *options = nullptr, const char *level = "expert") { json_object *options = nullptr, const char *level = "expert", json_object *visibleWhen = nullptr) { 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)); Loading @@ -213,6 +218,7 @@ extern "C" { if (placeholder) json_object_object_add(f, "placeholder", json_object_new_string(placeholder)); json_object_object_add(f, "target", json_object_new_string(target)); if (options) json_object_object_add(f, "options", options); if (visibleWhen) json_object_object_add(f, "visibleWhen", visibleWhen); json_object_object_add(f, "level", json_object_new_string(level)); json_object_array_add(arr, f); }; Loading @@ -221,6 +227,12 @@ extern "C" { for (auto v : vals) json_object_array_add(a, json_object_new_string(v)); return a; }; auto mkVisible = [](const char *key, const char *value) { json_object *v = json_object_new_object(); json_object_object_add(v, "key", json_object_new_string(key)); json_object_object_add(v, "value", json_object_new_string(value)); return v; }; // Renders as four linked top/right/bottom/left inputs with a lock // toggle in the properties panel (see properties.js _renderSpacingBox) // instead of a single raw CSS shorthand string. Loading @@ -244,7 +256,16 @@ extern "C" { mkOpts({"ul", "ol"}), "simple"); addField("list_style_type", "List Style", "select", nullptr, "all", mkOpts({"", "disc", "circle", "square", "decimal", "decimal-leading-zero", "lower-alpha", "upper-alpha", "lower-roman", "upper-roman", "none", "emoji_circle"}), "simple"); "lower-alpha", "upper-alpha", "lower-roman", "upper-roman", "none", "emoji_circle", "arrow", "svg"}), "simple"); addField("svg_marker_icon_id", "Marker Icon (from Library)", "icon_browse", nullptr, "all", nullptr, "simple", mkVisible("list_style_type", "svg")); { // Tells the icon_browse picker (properties.js) which sibling field // holds the resolved SVG source — Render() only ever reads svgMarkerSvg. json_object *f = json_object_array_get_idx(arr, json_object_array_length(arr) - 1); json_object_object_add(f, "svgTargetKey", json_object_new_string("svg_marker_svg")); } addField("svg_marker_svg", "", "hidden"); addField("direction", "Direction", "select", nullptr, "desktop", mkOpts({"vertical", "horizontal"})); addField("gap", "Gap", "text", "8px"); Loading Loading @@ -301,6 +322,7 @@ extern "C" { element->SetAttribute("type_id", getTypeId().c_str()); element->SetAttribute("list_type", listType.c_str()); element->SetAttribute("list_style_type", listStyleType.c_str()); element->SetAttribute("svg_marker_icon_id", svgMarkerIconId.c_str()); element->SetAttribute("direction", direction.c_str()); element->SetAttribute("gap", gap.c_str()); element->SetAttribute("align_items", alignItems.c_str()); Loading Loading @@ -342,6 +364,12 @@ extern "C" { if (!dataAttrs.empty()) element->SetAttribute("data_attrs", dataAttrs.c_str()); if (!svgMarkerSvg.empty()) { tinyxml2::XMLElement* svgEl = doc->NewElement("SvgMarkerSvg"); svgEl->SetText(svgMarkerSvg.c_str()); element->InsertEndChild(svgEl); } // Store items as child <Item> elements for (const auto &item : items) { tinyxml2::XMLElement *li = doc->NewElement("Item"); Loading @@ -357,6 +385,7 @@ extern "C" { val = xml_data->Attribute("list_type"); if (val) listType = val; val = xml_data->Attribute("list_style_type"); if (val) listStyleType = val; val = xml_data->Attribute("svg_marker_icon_id"); if (val) svgMarkerIconId = val; val = xml_data->Attribute("direction"); if (val) direction = val; val = xml_data->Attribute("gap"); if (val) gap = val; val = xml_data->Attribute("align_items"); if (val) alignItems = val; Loading Loading @@ -425,6 +454,12 @@ extern "C" { val = xml_data->Attribute("data_attrs"); if (val) dataAttrs = val; tinyxml2::XMLElement* svgEl = xml_data->FirstChildElement("SvgMarkerSvg"); if (svgEl) { const char* svgVal = svgEl->GetText(); if (svgVal) svgMarkerSvg = svgVal; } // Read items from child <Item> elements items.clear(); for (tinyxml2::XMLElement *li = xml_data->FirstChildElement("Item"); li; li = li->NextSiblingElement("Item")) { Loading @@ -450,6 +485,8 @@ extern "C" { json_object *obj = json_object_new_object(); json_object_object_add(obj, "list_type", json_object_new_string(listType.c_str())); json_object_object_add(obj, "list_style_type", json_object_new_string(listStyleType.c_str())); json_object_object_add(obj, "svg_marker_icon_id", json_object_new_string(svgMarkerIconId.c_str())); json_object_object_add(obj, "svg_marker_svg", json_object_new_string(svgMarkerSvg.c_str())); json_object_object_add(obj, "direction", json_object_new_string(direction.c_str())); json_object_object_add(obj, "gap", json_object_new_string(gap.c_str())); json_object_object_add(obj, "align_items", json_object_new_string(alignItems.c_str())); Loading Loading @@ -507,6 +544,12 @@ extern "C" { if (json_object_object_get_ex(obj, "list_style_type", &val_obj)) { val = json_object_get_string(val_obj); if (val) listStyleType = val; } if (json_object_object_get_ex(obj, "svg_marker_icon_id", &val_obj)) { val = json_object_get_string(val_obj); if (val) svgMarkerIconId = val; } if (json_object_object_get_ex(obj, "svg_marker_svg", &val_obj)) { val = json_object_get_string(val_obj); if (val) svgMarkerSvg = val; } if (json_object_object_get_ex(obj, "direction", &val_obj)) { val = json_object_get_string(val_obj); if (val && val[0]) direction = val; } Loading Loading @@ -652,7 +695,9 @@ extern "C" { if (!fontColor.empty()) style += " color: " + fontColor + ";"; if (!lineHeight.empty()) style += " line-height: " + lineHeight + ";"; bool useEmojiMarker = (listStyleType == "emoji_circle"); if (useEmojiMarker) style += " list-style-type: none;"; bool useArrowMarker = (listStyleType == "arrow"); bool useSvgMarker = (listStyleType == "svg" && !svgMarkerSvg.empty()); if (useEmojiMarker || useArrowMarker || useSvgMarker) style += " list-style-type: none;"; else if (!listStyleType.empty()) style += " list-style-type: " + listStyleType + ";"; std::string paddingShorthand = joinSpacingShorthand(paddingTop, paddingRight, paddingBottom, paddingLeft); if (!paddingShorthand.empty()) style += " padding: " + paddingShorthand + ";"; Loading @@ -668,11 +713,29 @@ extern "C" { for (const auto &item : items) { libhtmlpp::HtmlElement li("li"); if (useEmojiMarker) { if (useEmojiMarker || useArrowMarker || useSvgMarker) { li.setAttribute("style", "display: flex; align-items: center; gap: 0.5em;"); libhtmlpp::HtmlElement marker("span"); marker.setAttribute("style", "flex: none;"); marker.appendChild(libhtmlpp::TextElement("❗")); if (useSvgMarker) { // Re-wrap the picked Icon Library SVG at a fixed "1em" // slot size, following currentColor so the marker // inherits the list's font_color (mirrors Textbox's // custom-icon Render()). std::string viewBox, inner; svgmarkup::parse(svgMarkerSvg, viewBox, inner); std::string vb = viewBox.empty() ? "0 0 24 24" : viewBox; std::string svgHtml = "<svg width=\"1em\" height=\"1em\" viewBox=\"" + vb + "\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">" + inner + "</svg>"; try { libhtmlpp::HtmlString svgBuf(svgHtml); libhtmlpp::Element &parsedSvg = svgBuf.parse(); marker.appendChild(parsedSvg); } catch (...) {} } else { marker.appendChild(libhtmlpp::TextElement(useArrowMarker ? "➤" : "❗")); } li.appendChild(marker); } if (!item.empty()) { Loading