Loading editor/src/htmlimport.cpp +79 −2 Original line number Diff line number Diff line Loading @@ -898,8 +898,7 @@ std::string blogi::htmlimport::extractCustomCss( std::string blogi::htmlimport::serializeElement(const libhtmlpp::Element *el) { if (!el) return ""; if (el->getType() == libhtmlpp::HtmlEl || el->getType() == libhtmlpp::SvgEL || el->getType() == libhtmlpp::TextAreaEL) { if (el->getType() == libhtmlpp::HtmlEl || el->getType() == libhtmlpp::TextAreaEL) { const libhtmlpp::HtmlElement *hel = static_cast<const libhtmlpp::HtmlElement*>(el); std::string tag = hel->getTagname(); std::string result = "<" + tag; Loading @@ -914,6 +913,33 @@ std::string blogi::htmlimport::serializeElement(const libhtmlpp::Element *el) { result += serializeInnerHtml(const_cast<libhtmlpp::HtmlElement*>(hel)); result += "</" + tag + ">"; return result; } else if (el->getType() == libhtmlpp::SvgEL) { // Unlike a plain HtmlElement, SvgElement stores its inner markup as // a raw blob (getSvg()) rather than a real child tree -- its own // insertChild/appendChild are deleted outright, and firstChild() // always returns null. Reusing the HtmlEl branch above (which walks // firstChild() via serializeInnerHtml) silently produced an empty // "<svg ...></svg>" shell for every icon on this class of // page-builder site (icons wrap inline <svg><path>/<polygon>... // markup, e.g. the header's location/email/phone icons) -- the // outer tag/attributes came through fine, only the actual icon // artwork vanished. Mirrors the ScriptEL branch below, which // already gets this right for the same reason (getScript()). const libhtmlpp::SvgElement *sel = static_cast<const libhtmlpp::SvgElement*>(el); std::string tag = sel->getTagname(); std::string result = "<" + tag; for (const auto *attr = sel->firstAttribute(); attr; attr = attr->nextAttribute()) { result += " " + attr->getKey(); std::string val = attr->getValue(); if (!val.empty()) { result += "=\"" + val + "\""; } } result += ">"; std::vector<char> svg = const_cast<libhtmlpp::SvgElement*>(sel)->getSvg(); result.append(svg.begin(), svg.end()); result += "</" + tag + ">"; return result; } else if (el->getType() == libhtmlpp::TextEl) { return const_cast<libhtmlpp::TextElement*>( static_cast<const libhtmlpp::TextElement*>(el))->getText(); Loading Loading @@ -1804,6 +1830,57 @@ void blogi::htmlimport::htmlElementToWidgetXml( if (child) childFrames.push_back({child, w, nullptr, elementEnv, childAncestorChain}); } } else if (tag == "svg") { // No dedicated widget type for a raw <svg> icon -- it becomes // CustomHtml below, same as the generic fallback, EXCEPT: a // page-builder icon like this commonly has no width/height // attribute of its own (just a viewBox), sized purely by an // external class-based CSS rule such as // ".con-kit-component-icon svg{width:...;height:...}". That // rule can never survive import: htmlimport's own "global // CSS" extraction only ever keeps :root/html/body/*-scoped // rules, and even per-element CSS matching deliberately // rejects a bare-tag target after a combinator like this // (see compoundMatches's own doc comment -- a real, previously // fixed hazard, not an oversight). Confirmed on a real page: // every header icon (location/email/phone) rendered as an // empty circle, collapsed to 0x0, with its <path>/<polygon> // artwork otherwise intact (see serializeElement's SvgEL // branch). Falling back to the svg's own viewBox dimensions // when neither a width/height attribute nor a matched // "width"/"height" cssProps entry exists at least makes the // icon visible -- not pixel-identical to the source page, // but far closer than invisible. std::string svgHtml = serializeElement(cur); if (elemAttrs.count("width") == 0 && elemAttrs.count("height") == 0 && !cssProps.count("width") && !cssProps.count("height")) { auto vbIt = elemAttrs.find("viewBox"); if (vbIt != elemAttrs.end()) { std::istringstream vb(vbIt->second); double minX, minY, vbWidth, vbHeight; if ((vb >> minX >> minY >> vbWidth >> vbHeight) && vbWidth > 0 && vbHeight > 0) { auto fmt = [](double v) { long long i = static_cast<long long>(v); return (v == static_cast<double>(i)) ? std::to_string(i) : std::to_string(v); }; std::string sizeAttrs = " width=\"" + fmt(vbWidth) + "\" height=\"" + fmt(vbHeight) + "\""; size_t tagEnd = svgHtml.find("<svg"); if (tagEnd != std::string::npos) svgHtml.insert(tagEnd + 4, sizeAttrs); } } } tinyxml2::XMLElement *w = doc.NewElement("CustomHtml"); tinyxml2::XMLElement *htmlContent = doc.NewElement("HtmlContent"); htmlContent->SetText(svgHtml.c_str()); w->InsertEndChild(htmlContent); if (!style.empty()) { tinyxml2::XMLElement *cssEl = doc.NewElement("CssContent"); cssEl->SetText(style.c_str()); w->InsertEndChild(cssEl); } pXml->InsertEndChild(w); } else if (tag == "h1" || tag == "h2" || tag == "h3" || tag == "h4" || tag == "h5" || tag == "h6" || tag == "p" || tag == "span" || tag == "b" || tag == "i" || tag == "em" || tag == "strong" || Loading tests/htmlimport_smoke.cpp +39 −0 Original line number Diff line number Diff line Loading @@ -509,6 +509,45 @@ int main(){ "an attribute not referenced by any media rule is not preserved"); } // --- serializeElement: <svg> icon content survives the CustomHtml // fallback --- // SvgElement stores its inner markup as a raw blob (getSvg()), not a // real child tree (its own insertChild/appendChild are deleted, and // firstChild() always returns null) -- serializeElement's generic // HtmlEl branch walked firstChild() via serializeInnerHtml and silently // produced an empty "<svg ...></svg>" shell, dropping every icon's // actual <path>/<polygon> artwork (real-world symptom: header // location/email/phone icons rendered as bare circles with nothing // inside). std::cout << "=== <svg> icon content preserved through CustomHtml fallback ===" << std::endl; { std::string html = "<div class=\"icon\"><svg viewBox=\"0 0 32 32\">" "<path d=\"M1,2\"></path><polygon points=\"1,2 3,4\"></polygon>" "</svg></div>"; std::string xml = importXml(html); check(xml.find("path") != std::string::npos, "the <path> child survives import (was silently dropped -- empty <svg></svg> shell)"); check(xml.find("polygon") != std::string::npos, "the <polygon> child survives import"); check(xml.find("width=\"32\" height=\"32\"") != std::string::npos, "an <svg> with no width/height attribute of its own falls back to its viewBox " "dimensions (real-world symptom: icons sized purely by an external CSS rule that " "can't survive import collapsed to an invisible 0x0 box)"); } { // An <svg> that already specifies its own width/height is left // alone -- the viewBox fallback only fires when neither is present. std::string html = "<div class=\"icon\"><svg viewBox=\"0 0 32 32\" width=\"18\" height=\"18\">" "<path d=\"M1,2\"></path></svg></div>"; std::string xml = importXml(html); check(xml.find("width=\"18\" height=\"18\"") != std::string::npos, "an <svg> with its own explicit width/height keeps it, unmodified"); check(xml.find("width=\"32\"") == std::string::npos, "the viewBox fallback does not also add its own width/height alongside the real one"); } // --- Summary --- std::cout << "\n=== " << passCount << "/" << testCount << " tests passed ===" << std::endl; Loading Loading
editor/src/htmlimport.cpp +79 −2 Original line number Diff line number Diff line Loading @@ -898,8 +898,7 @@ std::string blogi::htmlimport::extractCustomCss( std::string blogi::htmlimport::serializeElement(const libhtmlpp::Element *el) { if (!el) return ""; if (el->getType() == libhtmlpp::HtmlEl || el->getType() == libhtmlpp::SvgEL || el->getType() == libhtmlpp::TextAreaEL) { if (el->getType() == libhtmlpp::HtmlEl || el->getType() == libhtmlpp::TextAreaEL) { const libhtmlpp::HtmlElement *hel = static_cast<const libhtmlpp::HtmlElement*>(el); std::string tag = hel->getTagname(); std::string result = "<" + tag; Loading @@ -914,6 +913,33 @@ std::string blogi::htmlimport::serializeElement(const libhtmlpp::Element *el) { result += serializeInnerHtml(const_cast<libhtmlpp::HtmlElement*>(hel)); result += "</" + tag + ">"; return result; } else if (el->getType() == libhtmlpp::SvgEL) { // Unlike a plain HtmlElement, SvgElement stores its inner markup as // a raw blob (getSvg()) rather than a real child tree -- its own // insertChild/appendChild are deleted outright, and firstChild() // always returns null. Reusing the HtmlEl branch above (which walks // firstChild() via serializeInnerHtml) silently produced an empty // "<svg ...></svg>" shell for every icon on this class of // page-builder site (icons wrap inline <svg><path>/<polygon>... // markup, e.g. the header's location/email/phone icons) -- the // outer tag/attributes came through fine, only the actual icon // artwork vanished. Mirrors the ScriptEL branch below, which // already gets this right for the same reason (getScript()). const libhtmlpp::SvgElement *sel = static_cast<const libhtmlpp::SvgElement*>(el); std::string tag = sel->getTagname(); std::string result = "<" + tag; for (const auto *attr = sel->firstAttribute(); attr; attr = attr->nextAttribute()) { result += " " + attr->getKey(); std::string val = attr->getValue(); if (!val.empty()) { result += "=\"" + val + "\""; } } result += ">"; std::vector<char> svg = const_cast<libhtmlpp::SvgElement*>(sel)->getSvg(); result.append(svg.begin(), svg.end()); result += "</" + tag + ">"; return result; } else if (el->getType() == libhtmlpp::TextEl) { return const_cast<libhtmlpp::TextElement*>( static_cast<const libhtmlpp::TextElement*>(el))->getText(); Loading Loading @@ -1804,6 +1830,57 @@ void blogi::htmlimport::htmlElementToWidgetXml( if (child) childFrames.push_back({child, w, nullptr, elementEnv, childAncestorChain}); } } else if (tag == "svg") { // No dedicated widget type for a raw <svg> icon -- it becomes // CustomHtml below, same as the generic fallback, EXCEPT: a // page-builder icon like this commonly has no width/height // attribute of its own (just a viewBox), sized purely by an // external class-based CSS rule such as // ".con-kit-component-icon svg{width:...;height:...}". That // rule can never survive import: htmlimport's own "global // CSS" extraction only ever keeps :root/html/body/*-scoped // rules, and even per-element CSS matching deliberately // rejects a bare-tag target after a combinator like this // (see compoundMatches's own doc comment -- a real, previously // fixed hazard, not an oversight). Confirmed on a real page: // every header icon (location/email/phone) rendered as an // empty circle, collapsed to 0x0, with its <path>/<polygon> // artwork otherwise intact (see serializeElement's SvgEL // branch). Falling back to the svg's own viewBox dimensions // when neither a width/height attribute nor a matched // "width"/"height" cssProps entry exists at least makes the // icon visible -- not pixel-identical to the source page, // but far closer than invisible. std::string svgHtml = serializeElement(cur); if (elemAttrs.count("width") == 0 && elemAttrs.count("height") == 0 && !cssProps.count("width") && !cssProps.count("height")) { auto vbIt = elemAttrs.find("viewBox"); if (vbIt != elemAttrs.end()) { std::istringstream vb(vbIt->second); double minX, minY, vbWidth, vbHeight; if ((vb >> minX >> minY >> vbWidth >> vbHeight) && vbWidth > 0 && vbHeight > 0) { auto fmt = [](double v) { long long i = static_cast<long long>(v); return (v == static_cast<double>(i)) ? std::to_string(i) : std::to_string(v); }; std::string sizeAttrs = " width=\"" + fmt(vbWidth) + "\" height=\"" + fmt(vbHeight) + "\""; size_t tagEnd = svgHtml.find("<svg"); if (tagEnd != std::string::npos) svgHtml.insert(tagEnd + 4, sizeAttrs); } } } tinyxml2::XMLElement *w = doc.NewElement("CustomHtml"); tinyxml2::XMLElement *htmlContent = doc.NewElement("HtmlContent"); htmlContent->SetText(svgHtml.c_str()); w->InsertEndChild(htmlContent); if (!style.empty()) { tinyxml2::XMLElement *cssEl = doc.NewElement("CssContent"); cssEl->SetText(style.c_str()); w->InsertEndChild(cssEl); } pXml->InsertEndChild(w); } else if (tag == "h1" || tag == "h2" || tag == "h3" || tag == "h4" || tag == "h5" || tag == "h6" || tag == "p" || tag == "span" || tag == "b" || tag == "i" || tag == "em" || tag == "strong" || Loading
tests/htmlimport_smoke.cpp +39 −0 Original line number Diff line number Diff line Loading @@ -509,6 +509,45 @@ int main(){ "an attribute not referenced by any media rule is not preserved"); } // --- serializeElement: <svg> icon content survives the CustomHtml // fallback --- // SvgElement stores its inner markup as a raw blob (getSvg()), not a // real child tree (its own insertChild/appendChild are deleted, and // firstChild() always returns null) -- serializeElement's generic // HtmlEl branch walked firstChild() via serializeInnerHtml and silently // produced an empty "<svg ...></svg>" shell, dropping every icon's // actual <path>/<polygon> artwork (real-world symptom: header // location/email/phone icons rendered as bare circles with nothing // inside). std::cout << "=== <svg> icon content preserved through CustomHtml fallback ===" << std::endl; { std::string html = "<div class=\"icon\"><svg viewBox=\"0 0 32 32\">" "<path d=\"M1,2\"></path><polygon points=\"1,2 3,4\"></polygon>" "</svg></div>"; std::string xml = importXml(html); check(xml.find("path") != std::string::npos, "the <path> child survives import (was silently dropped -- empty <svg></svg> shell)"); check(xml.find("polygon") != std::string::npos, "the <polygon> child survives import"); check(xml.find("width=\"32\" height=\"32\"") != std::string::npos, "an <svg> with no width/height attribute of its own falls back to its viewBox " "dimensions (real-world symptom: icons sized purely by an external CSS rule that " "can't survive import collapsed to an invisible 0x0 box)"); } { // An <svg> that already specifies its own width/height is left // alone -- the viewBox fallback only fires when neither is present. std::string html = "<div class=\"icon\"><svg viewBox=\"0 0 32 32\" width=\"18\" height=\"18\">" "<path d=\"M1,2\"></path></svg></div>"; std::string xml = importXml(html); check(xml.find("width=\"18\" height=\"18\"") != std::string::npos, "an <svg> with its own explicit width/height keeps it, unmodified"); check(xml.find("width=\"32\"") == std::string::npos, "the viewBox fallback does not also add its own width/height alongside the real one"); } // --- Summary --- std::cout << "\n=== " << passCount << "/" << testCount << " tests passed ===" << std::endl; Loading