Commit 2f4cc8f6 authored by jan.koester's avatar jan.koester
Browse files

test

parent 7fe4cba7
Loading
Loading
Loading
Loading
+144 −50
Changes for editor/src/htmlimport.cpp: 144 added lines, 50 removed lines.
Original line number Diff line number Diff line
@@ -243,13 +243,47 @@ bool blogi::htmlimport::fetchExternalResource(const std::string &url,
    return false;
}

namespace {

// Strips a trailing "!important" (case-insensitive, any whitespace before
// the "!") from a CSS declaration value, returning whether it was present.
// libhtmlpp::CSSDeclaration's parser captures "!important" as ordinary
// trailing text with no special handling, so left as-is it would end up as
// literal corrupted text in a widget attribute meant to hold a plain value
// (e.g. background_color="red !important"), and some rendering paths (e.g.
// JS "el.style.width = value") silently drop the whole assignment once the
// string contains this suffix instead of honoring just the priority hint.
bool stripImportant(std::string &value)
{
    size_t bang = value.rfind('!');
    if (bang == std::string::npos) return false;

    size_t start = value.find_first_not_of(" \t\n\r", bang + 1);
    size_t end = value.find_last_not_of(" \t\n\r");
    if (start == std::string::npos || end == std::string::npos || start > end) return false;

    std::string tail = value.substr(start, end - start + 1);
    std::transform(tail.begin(), tail.end(), tail.begin(),
        [](unsigned char c) { return std::tolower(c); });
    if (tail != "important") return false;

    size_t valEnd = bang;
    while (valEnd > 0 && std::isspace(static_cast<unsigned char>(value[valEnd - 1]))) --valEnd;
    value.resize(valEnd);
    return true;
}

} // namespace

std::map<std::string,std::string>
blogi::htmlimport::declarationToMap(
    const libhtmlpp::CSSDeclaration &decl)
{
    std::map<std::string,std::string> result;
    for (const auto &prop : decl.getProperties()) {
        result[prop.getName()] = prop.getValue();
        std::string value = prop.getValue();
        stripImportant(value);
        result[prop.getName()] = value;
    }
    return result;
}
@@ -272,6 +306,15 @@ void blogi::htmlimport::mergeStylesheetRules(
        }
    }

    // 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;
@@ -328,56 +371,78 @@ void blogi::htmlimport::mergeStylesheetRules(

            bool matches = false;

            // A bare tag name left over after stripping a combinator's
            // ancestor part (e.g. "[data-atom=header]>div" reduces to "div")
            // can't be verified here -- 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 on the rightmost compound (e.g. "div.foo")
            // is still specific enough to check below.
            bool bareTagAfterCombinator = hadCombinator &&
                !matchSel.empty() && matchSel[0] != '#' && matchSel[0] != '.' &&
                matchSel.find('.') == std::string::npos &&
                matchSel.find('#') == std::string::npos;

            if (bareTagAfterCombinator) {
                // matches stays false
            } else if (!matchSel.empty() && matchSel[0] == '#') {
                if (!id.empty() && matchSel == "#" + id) matches = true;
            } else if (!matchSel.empty() && matchSel[0] == '.') {
                std::string clsSel = matchSel.substr(1);
                for (const auto &c : classes) {
                    if (c == clsSel) { matches = true; break; }
                }
            // 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 {
                std::string selLower = matchSel;
                std::transform(selLower.begin(), selLower.end(), selLower.begin(),
                    [](unsigned char c) { return std::tolower(c); });
                if (selLower == tag) matches = true;

                auto dotPos = matchSel.find('.');
                if (!matches && dotPos != std::string::npos) {
                    std::string tagPart = matchSel.substr(0, dotPos);
                    std::string clsPart = matchSel.substr(dotPos + 1);
                    std::transform(tagPart.begin(), tagPart.end(), tagPart.begin(),
                        [](unsigned char c) { return std::tolower(c); });
                    if (tagPart == tag) {
                        for (const auto &c : classes) {
                            if (c == clsPart) { matches = true; break; }
                        }
                        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;
                    }
                }

                auto hashPos = matchSel.find('#');
                if (!matches && hashPos != std::string::npos) {
                    std::string tagPart = matchSel.substr(0, hashPos);
                    std::string idPart = matchSel.substr(hashPos + 1);
                std::transform(tagPart.begin(), tagPart.end(), tagPart.begin(),
                    [](unsigned char c) { return std::tolower(c); });
                    if (tagPart == tag && idPart == id) matches = true;

                // 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;
                }
            }

@@ -400,9 +465,21 @@ void blogi::htmlimport::mergeStylesheetRules(
                    }
                } else {
                    for (const auto &prop : rule.getDeclaration().getProperties()) {
                        if (props.find(prop.getName()) == props.end()) {
                            props[prop.getName()] = prop.getValue();
                        }
                        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);
                    }
                }
            }
@@ -1641,6 +1718,19 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html,
    while (!searchStack.empty()) {
        libhtmlpp::Element *cur = searchStack.top();
        searchStack.pop();

        // Children discovered while walking this frame's sibling chain are
        // collected here and pushed in reverse at the end (same pattern as
        // htmlElementToWidgetXml's childFrames) instead of being pushed onto
        // searchStack the moment they're found. Pushing immediately would
        // have the stack's LIFO order process the LAST sibling's subtree
        // before earlier siblings' (e.g. <body>'s <style> collected before
        // <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).
        std::vector<libhtmlpp::Element*> childFrames;

        while (cur) {
            if (cur->getType() == libhtmlpp::ScriptEL) {
                libhtmlpp::ScriptElement *sel = static_cast<libhtmlpp::ScriptElement*>(cur);
@@ -1766,11 +1856,15 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html,
                }
                if (tagName != "style") {
                    libhtmlpp::Element *child = hel->firstChild();
                    if (child) searchStack.push(child);
                    if (child) childFrames.push_back(child);
                }
            }
            cur = cur->nextElement();
        }

        for (auto it = childFrames.rbegin(); it != childFrames.rend(); ++it) {
            searchStack.push(*it);
        }
    }

    // Extract global CSS rules