Loading editor/src/htmlimport.cpp +98 −240 Changes for editor/src/htmlimport.cpp: 98 added lines, 240 removed lines. Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ *******************************************************************************/ #include "htmlimport.h" #include "plugin.h" #include <algorithm> #include <cctype> Loading Loading @@ -352,243 +353,89 @@ std::string extractBackgroundColorFromShorthand(const std::string &value) return candidateCount == 1 ? candidate : std::string(); } } // namespace std::map<std::string,std::string> blogi::htmlimport::declarationToMap( const libhtmlpp::CSSDeclaration &decl) // Derives "background-color" from a "background" shorthand value already // present in `props` -- see extractBackgroundColorFromShorthand's doc // comment for why: every widget's known-CSS mapping only ever looks for the // longhand "background-color", never "background" itself. This only ever // looks at the FINAL cascaded value of "background" (libhtmlpp::getCSSRules // has already resolved inline/stylesheet-rule precedence by the time this // runs), unlike the old per-rule derivation this replaces, which re-derived // mid-cascade for every matching rule that touched "background" -- a // simplification that only matters for the rare case of multiple // conflicting background/background-color rules with mixed !important on // the same element. void applyBackgroundColorShorthand(std::map<std::string,std::string> &props) { std::map<std::string,std::string> result; for (const auto &prop : decl.getProperties()) { std::string value = prop.getValue(); stripImportant(value); result[prop.getName()] = value; } if (result.count("background") && !result.count("background-color")) { std::string bg = extractBackgroundColorFromShorthand(result["background"]); if (!bg.empty()) result["background-color"] = bg; } return result; } void blogi::htmlimport::mergeStylesheetRules( const libhtmlpp::CSSStyleSheet &sheet, const std::string &tag, const std::string &cssClass, const std::string &id, std::map<std::string,std::string> &props, std::string &mediaRules, std::set<std::string> &seenMediaBlocks) if (props.count("background") && !props.count("background-color")) { std::string bg = extractBackgroundColorFromShorthand(props["background"]); if (!bg.empty()) props["background-color"] = bg; } } // Combines longhand CSS properties into the single shorthand key each // widget's XML schema actually has an attribute for -- run once per element, // after libhtmlpp::getCSSRules has already resolved cascade precedence, and // before the per-widget-tag mapping below (which only ever looks for // "border"/"padding"/"margin", never their longhand pieces -- see e.g. // textbox.cpp's border_style/padding_top.../margin_top... attributes). Only // fires when the shorthand key is itself absent -- if the source HTML // already set e.g. "border" directly, any longhand pieces also present are // left untouched and fall through to extractCustomCss unchanged (same as // before this function existed), since a page mixing shorthand and a // longhand override for one component (e.g. "border: 1px solid black; // border-color: red;") needs that override to keep applying, which erasing // it here would silently break. void normalizeShorthandProperties(std::map<std::string,std::string> &props) { std::vector<std::string> classes; if (!cssClass.empty()) { std::istringstream iss(cssClass); std::string cls; while (iss >> cls) { classes.push_back(cls); } } // Property names already present in @p props when we're called came from // this element's own inline style="..." (htmlElementToWidgetXml builds // props from inline style before calling this) -- inline style always // outranks a plain stylesheet rule, unless that rule is "!important" // (see isImportant below), matching real cascade precedence. std::set<std::string> inlineKeys; for (const auto &kv : props) inlineKeys.insert(kv.first); std::set<std::string> importantKeys; for (const auto &rule : sheet.getRules()) { const std::string &sel = rule.getSelector(); if (sel.empty()) continue; bool isAtRule = (!sel.empty() && sel[0] == '@'); std::string atWrapper; std::string innerSel; if (isAtRule) { size_t parenDepth = 0; size_t splitPos = std::string::npos; for (size_t i = 0; i < sel.size(); ++i) { if (sel[i] == '(') ++parenDepth; else if (sel[i] == ')') { if (parenDepth > 0) --parenDepth; if (parenDepth == 0) { splitPos = i + 1; break; } } } if (splitPos != std::string::npos && splitPos < sel.size()) { atWrapper = sel.substr(0, splitPos); size_t innerStart = sel.find_first_not_of(" \t\n\r", splitPos); if (innerStart != std::string::npos) { innerSel = sel.substr(innerStart); if (!props.count("border")) { std::string combined; auto append = [&](const char *key) { auto it = props.find(key); if (it != props.end() && !it->second.empty()) { if (!combined.empty()) combined += " "; combined += it->second; } }; append("border-width"); append("border-style"); append("border-color"); if (!combined.empty()) { props["border"] = combined; props.erase("border-width"); props.erase("border-style"); props.erase("border-color"); } } // A valid CSS box-model shorthand value must cover all four sides -- // there's no way to represent "only top and left were specified" without // inventing values for the other two, and guessing wrong is worse than // not filling in the shortcut attribute (same philosophy as // extractBackgroundColorFromShorthand). So this only fires when all four // longhand sides are present; a partial set is left as-is and falls // through to extractCustomCss individually, same as before. auto joinSides = [&](const char *shorthandKey, const char *topKey, const char *rightKey, const char *bottomKey, const char *leftKey) { if (props.count(shorthandKey)) return; if (!props.count(topKey) || !props.count(rightKey) || !props.count(bottomKey) || !props.count(leftKey)) return; // Reuses the widget-side helper (plugin.h) that migrates this exact // shorthand back into per-side attributes on load -- so this // synthesized value round-trips through the same, already-proven // path as a hand-authored legacy document. props[shorthandKey] = blogi::joinSpacingShorthand( props[topKey], props[rightKey], props[bottomKey], props[leftKey]); props.erase(topKey); props.erase(rightKey); props.erase(bottomKey); props.erase(leftKey); }; joinSides("padding", "padding-top", "padding-right", "padding-bottom", "padding-left"); joinSides("margin", "margin-top", "margin-right", "margin-bottom", "margin-left"); } if (innerSel.empty()) continue; } const std::string &matchTarget = isAtRule ? innerSel : sel; std::istringstream selStream(matchTarget); std::string singleSel; while (std::getline(selStream, singleSel, ',')) { size_t start = singleSel.find_first_not_of(" \t\n\r"); size_t end = singleSel.find_last_not_of(" \t\n\r"); if (start == std::string::npos) continue; singleSel = singleSel.substr(start, end - start + 1); std::string matchSel = singleSel; bool hadCombinator = false; { size_t lastSep = matchSel.find_last_of(" >+~"); if (lastSep != std::string::npos) { size_t mstart = matchSel.find_first_not_of(" >+~", lastSep); if (mstart != std::string::npos) { matchSel = matchSel.substr(mstart); hadCombinator = true; } } } bool matches = false; // Reject selector syntax this function can't safely evaluate -- // attribute selectors and pseudo-classes/elements describe // context (an ancestor's attribute) or state (":hover") that // this function has no way to check, and "*" would blanket- // match every element -- guessing wrong here is worse than not // matching at all (see bareTagAfterCombinator below for a // concrete case of exactly that going wrong). bool hasUnsupportedSyntax = matchSel.find('[') != std::string::npos || matchSel.find(':') != std::string::npos || matchSel == "*"; if (!hasUnsupportedSyntax) { // Splits a compound simple selector (combinators already // stripped above) into an optional bare tag, every ".class" // token, and an optional "#id" token -- e.g. // "div.card.featured#hero" -> tag=div, // classes={card,featured}, idPart=hero. A selector matches // only if EVERY part it specifies is present on this // element -- in particular every class listed, not just // one (fixes ".a.b" previously being compared as the single // literal string "a.b" against individual class tokens and // so never matching at all). std::string tagPart; std::vector<std::string> classParts; std::string idPart; size_t pos = 0; while (pos < matchSel.size()) { char c = matchSel[pos]; if (c == '.' || c == '#') { size_t next = matchSel.find_first_of(".#", pos + 1); size_t tokenEnd = (next == std::string::npos) ? matchSel.size() : next; std::string token = matchSel.substr(pos + 1, tokenEnd - (pos + 1)); if (c == '.') classParts.push_back(token); else idPart = token; pos = tokenEnd; } else { size_t next = matchSel.find_first_of(".#", pos); size_t tokenEnd = (next == std::string::npos) ? matchSel.size() : next; tagPart = matchSel.substr(pos, tokenEnd - pos); pos = tokenEnd; } } std::transform(tagPart.begin(), tagPart.end(), tagPart.begin(), [](unsigned char c) { return std::tolower(c); }); // A bare tag left over after stripping a combinator's // ancestor part (e.g. "[data-atom=header]>div" reduces to // "div") can't be verified -- this function only ever sees // the CURRENT element's own tag/class/id, never its // ancestor chain -- so treating it as a match would apply // the rule to literally every element of that tag in the // whole document. Confirmed against a real scraped page // where exactly this pattern matched ~24,000 times and blew // up the imported output to 36MB. A class/id qualifier // alongside the tag (e.g. "div.foo") is still specific // enough to check below. bool bareTagAfterCombinator = hadCombinator && classParts.empty() && idPart.empty() && !tagPart.empty(); bool specifiedSomething = !tagPart.empty() || !classParts.empty() || !idPart.empty(); if (!bareTagAfterCombinator && specifiedSomething) { bool tagOk = tagPart.empty() || tagPart == tag; bool idOk = idPart.empty() || idPart == id; bool classesOk = true; for (const auto &cls : classParts) { if (std::find(classes.begin(), classes.end(), cls) == classes.end()) { classesOk = false; break; } } if (tagOk && idOk && classesOk) matches = true; } } if (matches) { if (isAtRule) { std::string block = atWrapper + " { " + singleSel + " { "; for (const auto &prop : rule.getDeclaration().getProperties()) { block += prop.getName() + ": " + prop.getValue() + "; "; } block += "} } "; // The same @media block, once present anywhere in the // final page's HTML, applies document-wide regardless of // which widget's <style> tag carries it -- so including // it more than once across the whole import is pure // bloat, not a correctness requirement. Real scraped // pages routinely have many widgets share one rule (e.g. // by class), each independently re-matching it here. if (seenMediaBlocks.insert(block).second) { mediaRules += block; } } else { for (const auto &prop : rule.getDeclaration().getProperties()) { std::string value = prop.getValue(); bool isImportant = stripImportant(value); const std::string &key = prop.getName(); // Inline style and an already-!important value both // outrank a later plain rule; a later rule of equal // priority (both plain, or both !important) wins -- // approximating the real cascade's "last rule of // equal-or-higher precedence wins" without computing // full selector specificity. if (inlineKeys.count(key) && !isImportant) continue; if (importantKeys.count(key) && !isImportant) continue; props[key] = value; if (isImportant) importantKeys.insert(key); // See extractBackgroundColorFromShorthand's doc // comment -- widgets only ever look for the // longhand "background-color", never "background" // itself, so derive it here too under the exact // same priority rules as any other property match. if (key == "background") { std::string bg = extractBackgroundColorFromShorthand(value); if (!bg.empty()) { const std::string bgKey = "background-color"; bool skip = (inlineKeys.count(bgKey) && !isImportant) || (importantKeys.count(bgKey) && !isImportant); if (!skip) { props[bgKey] = bg; if (isImportant) importantKeys.insert(bgKey); } } } } } } } } } } // namespace std::string blogi::htmlimport::extractCustomCss( std::map<std::string,std::string> &props, Loading Loading @@ -851,16 +698,22 @@ void blogi::htmlimport::htmlElementToWidgetXml( continue; } // Get inline style // Raw inline style text -- kept for the CustomHtml fallback // branch further below, which embeds it verbatim regardless of // whether libhtmlpp could parse/match anything out of it. std::string style = el->getAtributte("style"); auto cssProps = declarationToMap( libhtmlpp::CSSStyleSheet::parseInlineStyle(style)); std::string cssClass = el->getAtributte("class"); std::string elemId = el->getAtributte("id"); std::string mediaRules; mergeStylesheetRules(sheet, tag, cssClass, elemId, cssProps, mediaRules, seenMediaBlocks); // Every matching rule for this element -- its own inline style, // every <style> block, and any externally fetched stylesheet // already folded into `sheet` below -- fully cascaded. auto cssRules = libhtmlpp::getCSSRules(*el, sheet, seenMediaBlocks); auto cssProps = cssRules.properties; std::string mediaRules = cssRules.mediaRules; applyBackgroundColorShorthand(cssProps); normalizeShorthandProperties(cssProps); // ---- Map HTML tags to widget types ---- Loading Loading @@ -1276,6 +1129,10 @@ void blogi::htmlimport::htmlElementToWidgetXml( w->SetAttribute("font_color", cssProps["color"].c_str()); if (cssProps.count("text-align")) w->SetAttribute("text_align", cssProps["text-align"].c_str()); if (cssProps.count("font-weight")) w->SetAttribute("font_weight", cssProps["font-weight"].c_str()); if (cssProps.count("font-style")) w->SetAttribute("font_style", cssProps["font-style"].c_str()); if (!cssClass.empty()) w->SetAttribute("css_class", cssClass.c_str()); if (!elemId.empty()) Loading Loading @@ -1831,7 +1688,7 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html, // <head>'s), scrambling the order rules end up in `stylesheet` // relative to the real document -- which matters once merging two // rules for the same property falls back to "later one wins" (see // mergeStylesheetRules). // libhtmlpp::CSSStyleSheet::collectApproximateMatches). std::vector<libhtmlpp::Element*> childFrames; while (cur) { Loading Loading @@ -2032,7 +1889,8 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html, // emit "@media (...) :root { decls }", which is invalid CSS (the // at-rule needs its own wrapping braces around the nested rule). // Reconstruct the proper nested form instead, the same way // mergeStylesheetRules does for per-element @media matches. // libhtmlpp::CSSStyleSheet::collectApproximateMatches does for // per-element @media matches. globalCss += atWrapper + " {\n " + innerSel + " {\n"; for (const auto &prop : rule.getDeclaration().getProperties()) { globalCss += " " + prop.getName() + ": " + prop.getValue() + ";\n"; Loading @@ -2053,7 +1911,7 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html, // Shared across every top-level node imported below (not per-node) so // an @media block already emitted via one node's widgets isn't repeated // by another's -- see mergeStylesheetRules's dedup comment. // by another's -- see collectApproximateMatches's dedup comment. std::set<std::string> seenMediaBlocks; if (root) { Loading editor/src/htmlimport.h +1 −22 Changes for editor/src/htmlimport.h: 1 added line, 22 removed lines. Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ #include <htmlpp/html.h> #include <htmlpp/css.h> #include <htmlpp/htmlcss.h> #include <tinyxml2.h> namespace blogi { Loading Loading @@ -71,28 +72,6 @@ namespace htmlimport { int jsFailed = 0; }; std::map<std::string,std::string> declarationToMap( const libhtmlpp::CSSDeclaration &decl); /** @p seenMediaBlocks accumulates the exact text of every @media block * already appended to some element's @p mediaRules across the whole * import (see convertHtmlToWidgetXml/htmlElementToWidgetXml, which * share one instance across every element processed) -- a block whose * text is already present is skipped rather than appended again. This * is safe because a <style> block's effect is document-wide regardless * of which widget's XML happens to carry it, and it matters because a * rule matched by a real page can independently match thousands of * elements (e.g. by shared class), each otherwise re-appending the * identical block text. */ void mergeStylesheetRules( const libhtmlpp::CSSStyleSheet &sheet, const std::string &tag, const std::string &cssClass, const std::string &id, std::map<std::string,std::string> &props, std::string &mediaRules, std::set<std::string> &seenMediaBlocks); std::string extractCustomCss( std::map<std::string,std::string> &props, const std::vector<std::string> &knownKeys, Loading Loading
editor/src/htmlimport.cpp +98 −240 Changes for editor/src/htmlimport.cpp: 98 added lines, 240 removed lines. Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ *******************************************************************************/ #include "htmlimport.h" #include "plugin.h" #include <algorithm> #include <cctype> Loading Loading @@ -352,243 +353,89 @@ std::string extractBackgroundColorFromShorthand(const std::string &value) return candidateCount == 1 ? candidate : std::string(); } } // namespace std::map<std::string,std::string> blogi::htmlimport::declarationToMap( const libhtmlpp::CSSDeclaration &decl) // Derives "background-color" from a "background" shorthand value already // present in `props` -- see extractBackgroundColorFromShorthand's doc // comment for why: every widget's known-CSS mapping only ever looks for the // longhand "background-color", never "background" itself. This only ever // looks at the FINAL cascaded value of "background" (libhtmlpp::getCSSRules // has already resolved inline/stylesheet-rule precedence by the time this // runs), unlike the old per-rule derivation this replaces, which re-derived // mid-cascade for every matching rule that touched "background" -- a // simplification that only matters for the rare case of multiple // conflicting background/background-color rules with mixed !important on // the same element. void applyBackgroundColorShorthand(std::map<std::string,std::string> &props) { std::map<std::string,std::string> result; for (const auto &prop : decl.getProperties()) { std::string value = prop.getValue(); stripImportant(value); result[prop.getName()] = value; } if (result.count("background") && !result.count("background-color")) { std::string bg = extractBackgroundColorFromShorthand(result["background"]); if (!bg.empty()) result["background-color"] = bg; } return result; } void blogi::htmlimport::mergeStylesheetRules( const libhtmlpp::CSSStyleSheet &sheet, const std::string &tag, const std::string &cssClass, const std::string &id, std::map<std::string,std::string> &props, std::string &mediaRules, std::set<std::string> &seenMediaBlocks) if (props.count("background") && !props.count("background-color")) { std::string bg = extractBackgroundColorFromShorthand(props["background"]); if (!bg.empty()) props["background-color"] = bg; } } // Combines longhand CSS properties into the single shorthand key each // widget's XML schema actually has an attribute for -- run once per element, // after libhtmlpp::getCSSRules has already resolved cascade precedence, and // before the per-widget-tag mapping below (which only ever looks for // "border"/"padding"/"margin", never their longhand pieces -- see e.g. // textbox.cpp's border_style/padding_top.../margin_top... attributes). Only // fires when the shorthand key is itself absent -- if the source HTML // already set e.g. "border" directly, any longhand pieces also present are // left untouched and fall through to extractCustomCss unchanged (same as // before this function existed), since a page mixing shorthand and a // longhand override for one component (e.g. "border: 1px solid black; // border-color: red;") needs that override to keep applying, which erasing // it here would silently break. void normalizeShorthandProperties(std::map<std::string,std::string> &props) { std::vector<std::string> classes; if (!cssClass.empty()) { std::istringstream iss(cssClass); std::string cls; while (iss >> cls) { classes.push_back(cls); } } // Property names already present in @p props when we're called came from // this element's own inline style="..." (htmlElementToWidgetXml builds // props from inline style before calling this) -- inline style always // outranks a plain stylesheet rule, unless that rule is "!important" // (see isImportant below), matching real cascade precedence. std::set<std::string> inlineKeys; for (const auto &kv : props) inlineKeys.insert(kv.first); std::set<std::string> importantKeys; for (const auto &rule : sheet.getRules()) { const std::string &sel = rule.getSelector(); if (sel.empty()) continue; bool isAtRule = (!sel.empty() && sel[0] == '@'); std::string atWrapper; std::string innerSel; if (isAtRule) { size_t parenDepth = 0; size_t splitPos = std::string::npos; for (size_t i = 0; i < sel.size(); ++i) { if (sel[i] == '(') ++parenDepth; else if (sel[i] == ')') { if (parenDepth > 0) --parenDepth; if (parenDepth == 0) { splitPos = i + 1; break; } } } if (splitPos != std::string::npos && splitPos < sel.size()) { atWrapper = sel.substr(0, splitPos); size_t innerStart = sel.find_first_not_of(" \t\n\r", splitPos); if (innerStart != std::string::npos) { innerSel = sel.substr(innerStart); if (!props.count("border")) { std::string combined; auto append = [&](const char *key) { auto it = props.find(key); if (it != props.end() && !it->second.empty()) { if (!combined.empty()) combined += " "; combined += it->second; } }; append("border-width"); append("border-style"); append("border-color"); if (!combined.empty()) { props["border"] = combined; props.erase("border-width"); props.erase("border-style"); props.erase("border-color"); } } // A valid CSS box-model shorthand value must cover all four sides -- // there's no way to represent "only top and left were specified" without // inventing values for the other two, and guessing wrong is worse than // not filling in the shortcut attribute (same philosophy as // extractBackgroundColorFromShorthand). So this only fires when all four // longhand sides are present; a partial set is left as-is and falls // through to extractCustomCss individually, same as before. auto joinSides = [&](const char *shorthandKey, const char *topKey, const char *rightKey, const char *bottomKey, const char *leftKey) { if (props.count(shorthandKey)) return; if (!props.count(topKey) || !props.count(rightKey) || !props.count(bottomKey) || !props.count(leftKey)) return; // Reuses the widget-side helper (plugin.h) that migrates this exact // shorthand back into per-side attributes on load -- so this // synthesized value round-trips through the same, already-proven // path as a hand-authored legacy document. props[shorthandKey] = blogi::joinSpacingShorthand( props[topKey], props[rightKey], props[bottomKey], props[leftKey]); props.erase(topKey); props.erase(rightKey); props.erase(bottomKey); props.erase(leftKey); }; joinSides("padding", "padding-top", "padding-right", "padding-bottom", "padding-left"); joinSides("margin", "margin-top", "margin-right", "margin-bottom", "margin-left"); } if (innerSel.empty()) continue; } const std::string &matchTarget = isAtRule ? innerSel : sel; std::istringstream selStream(matchTarget); std::string singleSel; while (std::getline(selStream, singleSel, ',')) { size_t start = singleSel.find_first_not_of(" \t\n\r"); size_t end = singleSel.find_last_not_of(" \t\n\r"); if (start == std::string::npos) continue; singleSel = singleSel.substr(start, end - start + 1); std::string matchSel = singleSel; bool hadCombinator = false; { size_t lastSep = matchSel.find_last_of(" >+~"); if (lastSep != std::string::npos) { size_t mstart = matchSel.find_first_not_of(" >+~", lastSep); if (mstart != std::string::npos) { matchSel = matchSel.substr(mstart); hadCombinator = true; } } } bool matches = false; // Reject selector syntax this function can't safely evaluate -- // attribute selectors and pseudo-classes/elements describe // context (an ancestor's attribute) or state (":hover") that // this function has no way to check, and "*" would blanket- // match every element -- guessing wrong here is worse than not // matching at all (see bareTagAfterCombinator below for a // concrete case of exactly that going wrong). bool hasUnsupportedSyntax = matchSel.find('[') != std::string::npos || matchSel.find(':') != std::string::npos || matchSel == "*"; if (!hasUnsupportedSyntax) { // Splits a compound simple selector (combinators already // stripped above) into an optional bare tag, every ".class" // token, and an optional "#id" token -- e.g. // "div.card.featured#hero" -> tag=div, // classes={card,featured}, idPart=hero. A selector matches // only if EVERY part it specifies is present on this // element -- in particular every class listed, not just // one (fixes ".a.b" previously being compared as the single // literal string "a.b" against individual class tokens and // so never matching at all). std::string tagPart; std::vector<std::string> classParts; std::string idPart; size_t pos = 0; while (pos < matchSel.size()) { char c = matchSel[pos]; if (c == '.' || c == '#') { size_t next = matchSel.find_first_of(".#", pos + 1); size_t tokenEnd = (next == std::string::npos) ? matchSel.size() : next; std::string token = matchSel.substr(pos + 1, tokenEnd - (pos + 1)); if (c == '.') classParts.push_back(token); else idPart = token; pos = tokenEnd; } else { size_t next = matchSel.find_first_of(".#", pos); size_t tokenEnd = (next == std::string::npos) ? matchSel.size() : next; tagPart = matchSel.substr(pos, tokenEnd - pos); pos = tokenEnd; } } std::transform(tagPart.begin(), tagPart.end(), tagPart.begin(), [](unsigned char c) { return std::tolower(c); }); // A bare tag left over after stripping a combinator's // ancestor part (e.g. "[data-atom=header]>div" reduces to // "div") can't be verified -- this function only ever sees // the CURRENT element's own tag/class/id, never its // ancestor chain -- so treating it as a match would apply // the rule to literally every element of that tag in the // whole document. Confirmed against a real scraped page // where exactly this pattern matched ~24,000 times and blew // up the imported output to 36MB. A class/id qualifier // alongside the tag (e.g. "div.foo") is still specific // enough to check below. bool bareTagAfterCombinator = hadCombinator && classParts.empty() && idPart.empty() && !tagPart.empty(); bool specifiedSomething = !tagPart.empty() || !classParts.empty() || !idPart.empty(); if (!bareTagAfterCombinator && specifiedSomething) { bool tagOk = tagPart.empty() || tagPart == tag; bool idOk = idPart.empty() || idPart == id; bool classesOk = true; for (const auto &cls : classParts) { if (std::find(classes.begin(), classes.end(), cls) == classes.end()) { classesOk = false; break; } } if (tagOk && idOk && classesOk) matches = true; } } if (matches) { if (isAtRule) { std::string block = atWrapper + " { " + singleSel + " { "; for (const auto &prop : rule.getDeclaration().getProperties()) { block += prop.getName() + ": " + prop.getValue() + "; "; } block += "} } "; // The same @media block, once present anywhere in the // final page's HTML, applies document-wide regardless of // which widget's <style> tag carries it -- so including // it more than once across the whole import is pure // bloat, not a correctness requirement. Real scraped // pages routinely have many widgets share one rule (e.g. // by class), each independently re-matching it here. if (seenMediaBlocks.insert(block).second) { mediaRules += block; } } else { for (const auto &prop : rule.getDeclaration().getProperties()) { std::string value = prop.getValue(); bool isImportant = stripImportant(value); const std::string &key = prop.getName(); // Inline style and an already-!important value both // outrank a later plain rule; a later rule of equal // priority (both plain, or both !important) wins -- // approximating the real cascade's "last rule of // equal-or-higher precedence wins" without computing // full selector specificity. if (inlineKeys.count(key) && !isImportant) continue; if (importantKeys.count(key) && !isImportant) continue; props[key] = value; if (isImportant) importantKeys.insert(key); // See extractBackgroundColorFromShorthand's doc // comment -- widgets only ever look for the // longhand "background-color", never "background" // itself, so derive it here too under the exact // same priority rules as any other property match. if (key == "background") { std::string bg = extractBackgroundColorFromShorthand(value); if (!bg.empty()) { const std::string bgKey = "background-color"; bool skip = (inlineKeys.count(bgKey) && !isImportant) || (importantKeys.count(bgKey) && !isImportant); if (!skip) { props[bgKey] = bg; if (isImportant) importantKeys.insert(bgKey); } } } } } } } } } } // namespace std::string blogi::htmlimport::extractCustomCss( std::map<std::string,std::string> &props, Loading Loading @@ -851,16 +698,22 @@ void blogi::htmlimport::htmlElementToWidgetXml( continue; } // Get inline style // Raw inline style text -- kept for the CustomHtml fallback // branch further below, which embeds it verbatim regardless of // whether libhtmlpp could parse/match anything out of it. std::string style = el->getAtributte("style"); auto cssProps = declarationToMap( libhtmlpp::CSSStyleSheet::parseInlineStyle(style)); std::string cssClass = el->getAtributte("class"); std::string elemId = el->getAtributte("id"); std::string mediaRules; mergeStylesheetRules(sheet, tag, cssClass, elemId, cssProps, mediaRules, seenMediaBlocks); // Every matching rule for this element -- its own inline style, // every <style> block, and any externally fetched stylesheet // already folded into `sheet` below -- fully cascaded. auto cssRules = libhtmlpp::getCSSRules(*el, sheet, seenMediaBlocks); auto cssProps = cssRules.properties; std::string mediaRules = cssRules.mediaRules; applyBackgroundColorShorthand(cssProps); normalizeShorthandProperties(cssProps); // ---- Map HTML tags to widget types ---- Loading Loading @@ -1276,6 +1129,10 @@ void blogi::htmlimport::htmlElementToWidgetXml( w->SetAttribute("font_color", cssProps["color"].c_str()); if (cssProps.count("text-align")) w->SetAttribute("text_align", cssProps["text-align"].c_str()); if (cssProps.count("font-weight")) w->SetAttribute("font_weight", cssProps["font-weight"].c_str()); if (cssProps.count("font-style")) w->SetAttribute("font_style", cssProps["font-style"].c_str()); if (!cssClass.empty()) w->SetAttribute("css_class", cssClass.c_str()); if (!elemId.empty()) Loading Loading @@ -1831,7 +1688,7 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html, // <head>'s), scrambling the order rules end up in `stylesheet` // relative to the real document -- which matters once merging two // rules for the same property falls back to "later one wins" (see // mergeStylesheetRules). // libhtmlpp::CSSStyleSheet::collectApproximateMatches). std::vector<libhtmlpp::Element*> childFrames; while (cur) { Loading Loading @@ -2032,7 +1889,8 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html, // emit "@media (...) :root { decls }", which is invalid CSS (the // at-rule needs its own wrapping braces around the nested rule). // Reconstruct the proper nested form instead, the same way // mergeStylesheetRules does for per-element @media matches. // libhtmlpp::CSSStyleSheet::collectApproximateMatches does for // per-element @media matches. globalCss += atWrapper + " {\n " + innerSel + " {\n"; for (const auto &prop : rule.getDeclaration().getProperties()) { globalCss += " " + prop.getName() + ": " + prop.getValue() + ";\n"; Loading @@ -2053,7 +1911,7 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html, // Shared across every top-level node imported below (not per-node) so // an @media block already emitted via one node's widgets isn't repeated // by another's -- see mergeStylesheetRules's dedup comment. // by another's -- see collectApproximateMatches's dedup comment. std::set<std::string> seenMediaBlocks; if (root) { Loading
editor/src/htmlimport.h +1 −22 Changes for editor/src/htmlimport.h: 1 added line, 22 removed lines. Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ #include <htmlpp/html.h> #include <htmlpp/css.h> #include <htmlpp/htmlcss.h> #include <tinyxml2.h> namespace blogi { Loading Loading @@ -71,28 +72,6 @@ namespace htmlimport { int jsFailed = 0; }; std::map<std::string,std::string> declarationToMap( const libhtmlpp::CSSDeclaration &decl); /** @p seenMediaBlocks accumulates the exact text of every @media block * already appended to some element's @p mediaRules across the whole * import (see convertHtmlToWidgetXml/htmlElementToWidgetXml, which * share one instance across every element processed) -- a block whose * text is already present is skipped rather than appended again. This * is safe because a <style> block's effect is document-wide regardless * of which widget's XML happens to carry it, and it matters because a * rule matched by a real page can independently match thousands of * elements (e.g. by shared class), each otherwise re-appending the * identical block text. */ void mergeStylesheetRules( const libhtmlpp::CSSStyleSheet &sheet, const std::string &tag, const std::string &cssClass, const std::string &id, std::map<std::string,std::string> &props, std::string &mediaRules, std::set<std::string> &seenMediaBlocks); std::string extractCustomCss( std::map<std::string,std::string> &props, const std::vector<std::string> &knownKeys, Loading