Loading editor/src/htmlimport.cpp +98 −8 Changes for editor/src/htmlimport.cpp: 98 added lines, 8 removed lines. Original line number Diff line number Diff line Loading @@ -435,6 +435,47 @@ void normalizeShorthandProperties(std::map<std::string,std::string> &props) joinSides("margin", "margin-top", "margin-right", "margin-bottom", "margin-left"); } // True if this element's own CSS says it shouldn't render at all. Checked // against the widgets that have a real "hidden" boolean attribute (Section/ // Container/Grid/Article/List all do -- see each widget's own Render()) -- // "display" is otherwise always in these widgets' knownCss (dropped as // "handled" regardless of its value, same as before this existed), so only // the "none" case is worth singling out: it's the one value with a working, // reliable place to go, since a widget's own dedicated attribute isn't // subject to the CSS-specificity/scoping limitations of the CustomCss // fallback (which needs a class or id, and can lose to the widget's own // inline styling anyway). bool cssMeansHidden(const std::map<std::string,std::string> &props) { auto it = props.find("display"); if (it != props.end() && it->second == "none") return true; it = props.find("visibility"); if (it != props.end() && it->second == "hidden") return true; return false; } // Maps a CSS `justify-content`/`align-items` keyword value onto the // constrained 3-way ("top"/"center"/"bottom") or 4-way (+"stretch") // `vertical_align` select attribute that Container/Section/Article/Grid // actually have -- none of them accept raw CSS values directly. Values with // no equivalent in that enum (space-between, space-around, space-evenly, // baseline, or "stretch" when @p allowStretch is false) return "" rather // than being guessed; the caller leaves the source property out of // `knownCss` in that case, so it still falls through to extractCustomCss // instead of being silently dropped. std::string mapToVerticalAlign(const std::string &cssValue, bool allowStretch) { if (cssValue == "flex-start" || cssValue == "start" || cssValue == "left" || cssValue == "top") return "top"; if (cssValue == "center") return "center"; if (cssValue == "flex-end" || cssValue == "end" || cssValue == "right" || cssValue == "bottom") return "bottom"; if (allowStretch && cssValue == "stretch") return "stretch"; return ""; } } // namespace std::string blogi::htmlimport::extractCustomCss( Loading Loading @@ -744,11 +785,20 @@ void blogi::htmlimport::htmlElementToWidgetXml( w->SetAttribute("css_id", elemId.c_str()); if (semanticTag != "section") w->SetAttribute("semantic_tag", semanticTag.c_str()); if (cssMeansHidden(cssProps)) w->SetAttribute("hidden", "true"); std::vector<std::string> knownCss = { "background-color","border-radius","border","opacity", "padding","margin","max-width","min-height","width","height", "display","flex-direction","justify-content"}; "display","visibility"}; if (cssProps.count("justify-content")) { std::string va = mapToVerticalAlign(cssProps["justify-content"], false); if (!va.empty()) { w->SetAttribute("vertical_align", va.c_str()); knownCss.push_back("justify-content"); } } std::string custom = extractCustomCss(cssProps, knownCss, mediaRules, cssClass, elemId); if (!custom.empty()) { tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss"); Loading Loading @@ -777,11 +827,20 @@ void blogi::htmlimport::htmlElementToWidgetXml( w->SetAttribute("margin", cssProps["margin"].c_str()); if (!cssClass.empty()) w->SetAttribute("css_class", cssClass.c_str()); if (cssMeansHidden(cssProps)) w->SetAttribute("hidden", "true"); std::vector<std::string> knownCss = { "background-color","border-radius","border","opacity", "padding","margin","max-width","min-height","width","height", "display","flex-direction","justify-content"}; "display","visibility"}; if (cssProps.count("justify-content")) { std::string va = mapToVerticalAlign(cssProps["justify-content"], false); if (!va.empty()) { w->SetAttribute("vertical_align", va.c_str()); knownCss.push_back("justify-content"); } } std::string custom = extractCustomCss(cssProps, knownCss, mediaRules, cssClass, elemId); if (!custom.empty()) { tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss"); Loading Loading @@ -814,6 +873,8 @@ void blogi::htmlimport::htmlElementToWidgetXml( w->SetAttribute("css_class", cssClass.c_str()); if (!elemId.empty()) w->SetAttribute("css_id", elemId.c_str()); if (cssMeansHidden(cssProps)) w->SetAttribute("hidden", "true"); if (isGrid) { if (cssProps.count("grid-template-columns")) Loading @@ -824,8 +885,15 @@ void blogi::htmlimport::htmlElementToWidgetXml( w->SetAttribute("gap", cssProps["gap"].c_str()); std::vector<std::string> knownCss = { "background-color","border-radius","border","opacity", "padding","margin","width","height","display", "grid-template-columns","grid-template-rows","gap","align-items"}; "padding","margin","width","height","display","visibility", "grid-template-columns","grid-template-rows","gap"}; if (cssProps.count("align-items")) { std::string va = mapToVerticalAlign(cssProps["align-items"], true); if (!va.empty()) { w->SetAttribute("vertical_align", va.c_str()); knownCss.push_back("align-items"); } } std::string custom = extractCustomCss(cssProps, knownCss, mediaRules, cssClass, elemId); if (!custom.empty()) { tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss"); Loading @@ -835,8 +903,14 @@ void blogi::htmlimport::htmlElementToWidgetXml( } else { std::vector<std::string> knownCss = { "background-color","border-radius","border","opacity", "padding","margin","width","height","display", "flex-direction","justify-content"}; "padding","margin","width","height","display","visibility"}; if (cssProps.count("justify-content")) { std::string va = mapToVerticalAlign(cssProps["justify-content"], false); if (!va.empty()) { w->SetAttribute("vertical_align", va.c_str()); knownCss.push_back("justify-content"); } } std::string custom = extractCustomCss(cssProps, knownCss, mediaRules, cssClass, elemId); if (!custom.empty()) { tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss"); Loading Loading @@ -1020,6 +1094,22 @@ void blogi::htmlimport::htmlElementToWidgetXml( w->SetAttribute("padding", cssProps["padding"].c_str()); if (cssProps.count("margin")) w->SetAttribute("margin", cssProps["margin"].c_str()); if (cssMeansHidden(cssProps)) w->SetAttribute("hidden", "true"); // "row"/"column" match List's own "horizontal"/"vertical" // enum 1:1 in meaning even though the words differ. if (cssProps.count("flex-direction")) { const std::string &fd = cssProps["flex-direction"]; if (fd == "row" || fd == "row-reverse") w->SetAttribute("direction", "horizontal"); else if (fd == "column" || fd == "column-reverse") w->SetAttribute("direction", "vertical"); } // List's align_items field already uses raw CSS align-items // keywords (flex-start/center/flex-end/stretch/baseline), so // this is a direct passthrough, not a translation. if (cssProps.count("align-items")) w->SetAttribute("align_items", cssProps["align-items"].c_str()); libhtmlpp::Element *liEl = el->firstChild(); while (liEl) { Loading @@ -1041,8 +1131,8 @@ void blogi::htmlimport::htmlElementToWidgetXml( std::vector<std::string> knownCss = { "font-size","color","line-height","list-style-type", "padding","margin","width","height","display", "flex-direction","gap"}; "padding","margin","width","height","display","visibility", "flex-direction","gap","align-items"}; std::string custom = extractCustomCss(cssProps, knownCss, mediaRules, cssClass, elemId); if (!custom.empty()) { tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss"); Loading tests/htmlimport_smoke.cpp +81 −0 Changes for tests/htmlimport_smoke.cpp: 81 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -150,6 +150,87 @@ int main(){ "font-style reaches the TextBox's font_style attribute (previously silently dropped)"); } // --- display:none / visibility:hidden maps to the widget's hidden attr --- std::cout << "=== display:none / visibility:hidden ===" << std::endl; { std::string xml = importXml("<div style=\"display:none\">hi</div>"); check(xml.find("hidden=\"true\"") != std::string::npos, "display:none on a Container maps to hidden=\"true\" (previously silently dropped)"); } { std::string xml = importXml("<section style=\"visibility:hidden\">hi</section>"); check(xml.find("hidden=\"true\"") != std::string::npos, "visibility:hidden on a Section maps to hidden=\"true\""); } { std::string xml = importXml("<div style=\"display:flex\">hi</div>"); check(xml.find("hidden=\"true\"") == std::string::npos, "display:flex (not \"none\") does not mark the element hidden"); } { std::string xml = importXml("<article style=\"display:none\">hi</article>"); check(xml.find("hidden=\"true\"") != std::string::npos, "display:none on an Article maps to hidden=\"true\""); } { std::string xml = importXml( "<ul style=\"display:none\"><li>a</li></ul>"); check(xml.find("hidden=\"true\"") != std::string::npos, "display:none on a List maps to hidden=\"true\""); } { // "display" can only hold one value, so grid-ness is signaled by // display:grid while visibility:hidden (independent property) // triggers the hidden attribute without the two fighting over the // same CSS property. std::string html = "<div style=\"display:grid; grid-template-columns:1fr 1fr; visibility:hidden\">hi</div>"; std::string xml = importXml(html); check(xml.find("<Grid") != std::string::npos, "display:grid still produces a Grid widget"); check(xml.find("hidden=\"true\"") != std::string::npos, "visibility:hidden on a Grid maps to hidden=\"true\""); } // --- justify-content -> vertical_align (Container/Section/Article) --- std::cout << "=== justify-content -> vertical_align ===" << std::endl; { std::string xml = importXml("<div style=\"justify-content:center\">hi</div>"); check(xml.find("vertical_align=\"center\"") != std::string::npos, "justify-content:center maps to vertical_align=\"center\" on Container"); } { std::string xml = importXml("<div class=\"x\" style=\"justify-content:space-between\">hi</div>"); check(xml.find("vertical_align=") == std::string::npos, "justify-content:space-between has no vertical_align equivalent, isn't guessed"); check(xml.find("justify-content") != std::string::npos, "unmappable justify-content still falls through to CustomCss"); } // --- align-items -> vertical_align (Grid) / align_items (List) --- std::cout << "=== align-items ===" << std::endl; { std::string html = "<div style=\"display:grid; align-items:stretch\">hi</div>"; std::string xml = importXml(html); check(xml.find("vertical_align=\"stretch\"") != std::string::npos, "align-items:stretch maps to vertical_align=\"stretch\" on Grid"); } { std::string html = "<ul style=\"align-items:center\"><li>a</li></ul>"; std::string xml = importXml(html); check(xml.find("align_items=\"center\"") != std::string::npos, "align-items passes through directly to List's align_items attribute"); } // --- flex-direction -> direction (List only) --- std::cout << "=== flex-direction -> direction (List) ===" << std::endl; { std::string html = "<ul style=\"flex-direction:row\"><li>a</li></ul>"; std::string xml = importXml(html); check(xml.find("direction=\"horizontal\"") != std::string::npos, "flex-direction:row maps to List's direction=\"horizontal\""); } // --- Summary --- std::cout << "\n=== " << passCount << "/" << testCount << " tests passed ===" << std::endl; Loading Loading
editor/src/htmlimport.cpp +98 −8 Changes for editor/src/htmlimport.cpp: 98 added lines, 8 removed lines. Original line number Diff line number Diff line Loading @@ -435,6 +435,47 @@ void normalizeShorthandProperties(std::map<std::string,std::string> &props) joinSides("margin", "margin-top", "margin-right", "margin-bottom", "margin-left"); } // True if this element's own CSS says it shouldn't render at all. Checked // against the widgets that have a real "hidden" boolean attribute (Section/ // Container/Grid/Article/List all do -- see each widget's own Render()) -- // "display" is otherwise always in these widgets' knownCss (dropped as // "handled" regardless of its value, same as before this existed), so only // the "none" case is worth singling out: it's the one value with a working, // reliable place to go, since a widget's own dedicated attribute isn't // subject to the CSS-specificity/scoping limitations of the CustomCss // fallback (which needs a class or id, and can lose to the widget's own // inline styling anyway). bool cssMeansHidden(const std::map<std::string,std::string> &props) { auto it = props.find("display"); if (it != props.end() && it->second == "none") return true; it = props.find("visibility"); if (it != props.end() && it->second == "hidden") return true; return false; } // Maps a CSS `justify-content`/`align-items` keyword value onto the // constrained 3-way ("top"/"center"/"bottom") or 4-way (+"stretch") // `vertical_align` select attribute that Container/Section/Article/Grid // actually have -- none of them accept raw CSS values directly. Values with // no equivalent in that enum (space-between, space-around, space-evenly, // baseline, or "stretch" when @p allowStretch is false) return "" rather // than being guessed; the caller leaves the source property out of // `knownCss` in that case, so it still falls through to extractCustomCss // instead of being silently dropped. std::string mapToVerticalAlign(const std::string &cssValue, bool allowStretch) { if (cssValue == "flex-start" || cssValue == "start" || cssValue == "left" || cssValue == "top") return "top"; if (cssValue == "center") return "center"; if (cssValue == "flex-end" || cssValue == "end" || cssValue == "right" || cssValue == "bottom") return "bottom"; if (allowStretch && cssValue == "stretch") return "stretch"; return ""; } } // namespace std::string blogi::htmlimport::extractCustomCss( Loading Loading @@ -744,11 +785,20 @@ void blogi::htmlimport::htmlElementToWidgetXml( w->SetAttribute("css_id", elemId.c_str()); if (semanticTag != "section") w->SetAttribute("semantic_tag", semanticTag.c_str()); if (cssMeansHidden(cssProps)) w->SetAttribute("hidden", "true"); std::vector<std::string> knownCss = { "background-color","border-radius","border","opacity", "padding","margin","max-width","min-height","width","height", "display","flex-direction","justify-content"}; "display","visibility"}; if (cssProps.count("justify-content")) { std::string va = mapToVerticalAlign(cssProps["justify-content"], false); if (!va.empty()) { w->SetAttribute("vertical_align", va.c_str()); knownCss.push_back("justify-content"); } } std::string custom = extractCustomCss(cssProps, knownCss, mediaRules, cssClass, elemId); if (!custom.empty()) { tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss"); Loading Loading @@ -777,11 +827,20 @@ void blogi::htmlimport::htmlElementToWidgetXml( w->SetAttribute("margin", cssProps["margin"].c_str()); if (!cssClass.empty()) w->SetAttribute("css_class", cssClass.c_str()); if (cssMeansHidden(cssProps)) w->SetAttribute("hidden", "true"); std::vector<std::string> knownCss = { "background-color","border-radius","border","opacity", "padding","margin","max-width","min-height","width","height", "display","flex-direction","justify-content"}; "display","visibility"}; if (cssProps.count("justify-content")) { std::string va = mapToVerticalAlign(cssProps["justify-content"], false); if (!va.empty()) { w->SetAttribute("vertical_align", va.c_str()); knownCss.push_back("justify-content"); } } std::string custom = extractCustomCss(cssProps, knownCss, mediaRules, cssClass, elemId); if (!custom.empty()) { tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss"); Loading Loading @@ -814,6 +873,8 @@ void blogi::htmlimport::htmlElementToWidgetXml( w->SetAttribute("css_class", cssClass.c_str()); if (!elemId.empty()) w->SetAttribute("css_id", elemId.c_str()); if (cssMeansHidden(cssProps)) w->SetAttribute("hidden", "true"); if (isGrid) { if (cssProps.count("grid-template-columns")) Loading @@ -824,8 +885,15 @@ void blogi::htmlimport::htmlElementToWidgetXml( w->SetAttribute("gap", cssProps["gap"].c_str()); std::vector<std::string> knownCss = { "background-color","border-radius","border","opacity", "padding","margin","width","height","display", "grid-template-columns","grid-template-rows","gap","align-items"}; "padding","margin","width","height","display","visibility", "grid-template-columns","grid-template-rows","gap"}; if (cssProps.count("align-items")) { std::string va = mapToVerticalAlign(cssProps["align-items"], true); if (!va.empty()) { w->SetAttribute("vertical_align", va.c_str()); knownCss.push_back("align-items"); } } std::string custom = extractCustomCss(cssProps, knownCss, mediaRules, cssClass, elemId); if (!custom.empty()) { tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss"); Loading @@ -835,8 +903,14 @@ void blogi::htmlimport::htmlElementToWidgetXml( } else { std::vector<std::string> knownCss = { "background-color","border-radius","border","opacity", "padding","margin","width","height","display", "flex-direction","justify-content"}; "padding","margin","width","height","display","visibility"}; if (cssProps.count("justify-content")) { std::string va = mapToVerticalAlign(cssProps["justify-content"], false); if (!va.empty()) { w->SetAttribute("vertical_align", va.c_str()); knownCss.push_back("justify-content"); } } std::string custom = extractCustomCss(cssProps, knownCss, mediaRules, cssClass, elemId); if (!custom.empty()) { tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss"); Loading Loading @@ -1020,6 +1094,22 @@ void blogi::htmlimport::htmlElementToWidgetXml( w->SetAttribute("padding", cssProps["padding"].c_str()); if (cssProps.count("margin")) w->SetAttribute("margin", cssProps["margin"].c_str()); if (cssMeansHidden(cssProps)) w->SetAttribute("hidden", "true"); // "row"/"column" match List's own "horizontal"/"vertical" // enum 1:1 in meaning even though the words differ. if (cssProps.count("flex-direction")) { const std::string &fd = cssProps["flex-direction"]; if (fd == "row" || fd == "row-reverse") w->SetAttribute("direction", "horizontal"); else if (fd == "column" || fd == "column-reverse") w->SetAttribute("direction", "vertical"); } // List's align_items field already uses raw CSS align-items // keywords (flex-start/center/flex-end/stretch/baseline), so // this is a direct passthrough, not a translation. if (cssProps.count("align-items")) w->SetAttribute("align_items", cssProps["align-items"].c_str()); libhtmlpp::Element *liEl = el->firstChild(); while (liEl) { Loading @@ -1041,8 +1131,8 @@ void blogi::htmlimport::htmlElementToWidgetXml( std::vector<std::string> knownCss = { "font-size","color","line-height","list-style-type", "padding","margin","width","height","display", "flex-direction","gap"}; "padding","margin","width","height","display","visibility", "flex-direction","gap","align-items"}; std::string custom = extractCustomCss(cssProps, knownCss, mediaRules, cssClass, elemId); if (!custom.empty()) { tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss"); Loading
tests/htmlimport_smoke.cpp +81 −0 Changes for tests/htmlimport_smoke.cpp: 81 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -150,6 +150,87 @@ int main(){ "font-style reaches the TextBox's font_style attribute (previously silently dropped)"); } // --- display:none / visibility:hidden maps to the widget's hidden attr --- std::cout << "=== display:none / visibility:hidden ===" << std::endl; { std::string xml = importXml("<div style=\"display:none\">hi</div>"); check(xml.find("hidden=\"true\"") != std::string::npos, "display:none on a Container maps to hidden=\"true\" (previously silently dropped)"); } { std::string xml = importXml("<section style=\"visibility:hidden\">hi</section>"); check(xml.find("hidden=\"true\"") != std::string::npos, "visibility:hidden on a Section maps to hidden=\"true\""); } { std::string xml = importXml("<div style=\"display:flex\">hi</div>"); check(xml.find("hidden=\"true\"") == std::string::npos, "display:flex (not \"none\") does not mark the element hidden"); } { std::string xml = importXml("<article style=\"display:none\">hi</article>"); check(xml.find("hidden=\"true\"") != std::string::npos, "display:none on an Article maps to hidden=\"true\""); } { std::string xml = importXml( "<ul style=\"display:none\"><li>a</li></ul>"); check(xml.find("hidden=\"true\"") != std::string::npos, "display:none on a List maps to hidden=\"true\""); } { // "display" can only hold one value, so grid-ness is signaled by // display:grid while visibility:hidden (independent property) // triggers the hidden attribute without the two fighting over the // same CSS property. std::string html = "<div style=\"display:grid; grid-template-columns:1fr 1fr; visibility:hidden\">hi</div>"; std::string xml = importXml(html); check(xml.find("<Grid") != std::string::npos, "display:grid still produces a Grid widget"); check(xml.find("hidden=\"true\"") != std::string::npos, "visibility:hidden on a Grid maps to hidden=\"true\""); } // --- justify-content -> vertical_align (Container/Section/Article) --- std::cout << "=== justify-content -> vertical_align ===" << std::endl; { std::string xml = importXml("<div style=\"justify-content:center\">hi</div>"); check(xml.find("vertical_align=\"center\"") != std::string::npos, "justify-content:center maps to vertical_align=\"center\" on Container"); } { std::string xml = importXml("<div class=\"x\" style=\"justify-content:space-between\">hi</div>"); check(xml.find("vertical_align=") == std::string::npos, "justify-content:space-between has no vertical_align equivalent, isn't guessed"); check(xml.find("justify-content") != std::string::npos, "unmappable justify-content still falls through to CustomCss"); } // --- align-items -> vertical_align (Grid) / align_items (List) --- std::cout << "=== align-items ===" << std::endl; { std::string html = "<div style=\"display:grid; align-items:stretch\">hi</div>"; std::string xml = importXml(html); check(xml.find("vertical_align=\"stretch\"") != std::string::npos, "align-items:stretch maps to vertical_align=\"stretch\" on Grid"); } { std::string html = "<ul style=\"align-items:center\"><li>a</li></ul>"; std::string xml = importXml(html); check(xml.find("align_items=\"center\"") != std::string::npos, "align-items passes through directly to List's align_items attribute"); } // --- flex-direction -> direction (List only) --- std::cout << "=== flex-direction -> direction (List) ===" << std::endl; { std::string html = "<ul style=\"flex-direction:row\"><li>a</li></ul>"; std::string xml = importXml(html); check(xml.find("direction=\"horizontal\"") != std::string::npos, "flex-direction:row maps to List's direction=\"horizontal\""); } // --- Summary --- std::cout << "\n=== " << passCount << "/" << testCount << " tests passed ===" << std::endl; Loading