Commit edb9fe93 authored by jan.koester's avatar jan.koester
Browse files

test

parent 1272da5d
Loading
Loading
Loading
Loading
+139 −3
Original line number Diff line number Diff line
@@ -454,6 +454,116 @@ bool cssMeansHidden(const std::map<std::string,std::string> &props)
    return false;
}

// Splits a `class="a  b c"` attribute value on whitespace and adds each
// token to @p out.
void collectClassTokens(const std::string &classAttr, std::set<std::string> &out)
{
    std::istringstream iss(classAttr);
    std::string tok;
    while (iss >> tok) out.insert(tok);
}

// Best-effort scan for `class="..."`/`class='...'` occurrences in a raw
// (already-serialized) HTML fragment -- used on a <td>/<th>'s serialized
// inner HTML (see serializeInnerHtml) to also pick up classes on elements
// nested inside a table cell (e.g. the `<span class="ja">` badges used
// throughout hand-authored comparison tables), which never go through the
// normal per-element traversal since table cells are flattened to plain
// text/markup rather than recursed into like every other child element.
// Not a real HTML parser: it just looks for the literal `class=` attribute
// syntax, so it can't be fooled by nested quotes but also won't catch
// attribute values built some other way (there aren't any in HTML this
// scans, since it's always literal already-serialized markup).
void scanHtmlForClasses(const std::string &html, std::set<std::string> &out)
{
    size_t pos = 0;
    while ((pos = html.find("class=", pos)) != std::string::npos) {
        pos += 6;
        if (pos >= html.size()) break;
        char quote = html[pos];
        if (quote != '"' && quote != '\'') continue;
        size_t end = html.find(quote, pos + 1);
        if (end == std::string::npos) break;
        collectClassTokens(html.substr(pos + 1, end - pos - 1), out);
        pos = end + 1;
    }
}

// True if @p selector textually references @p cls as a whole class token
// (".cls", not just a substring match inside a longer class name).
bool selectorReferencesClass(const std::string &selector, const std::string &cls)
{
    std::string needle = "." + cls;
    size_t pos = 0;
    while ((pos = selector.find(needle, pos)) != std::string::npos) {
        size_t after = pos + needle.size();
        bool afterOk = after >= selector.size() ||
            !(std::isalnum(static_cast<unsigned char>(selector[after])) ||
              selector[after] == '-' || selector[after] == '_');
        bool beforeOk = pos == 0 ||
            !(std::isalnum(static_cast<unsigned char>(selector[pos - 1])) ||
              selector[pos - 1] == '-' || selector[pos - 1] == '_');
        if (afterOk && beforeOk) return true;
        pos = after;
    }
    return false;
}

// True if @p selector references one of the table-structural tag names
// (table/thead/tbody/tfoot/tr/td/th) as a whole word -- these apply purely
// by tag/nesting (e.g. "thead th", "tr.gruppe td") regardless of any
// specific class, so they're always relevant to a table's own CustomCss
// once its rows/cells preserve their original tag structure and classes
// (see the "table" branch of htmlElementToWidgetXml).
bool selectorReferencesTableStructure(const std::string &selector)
{
    static const std::set<std::string> kTags = {
        "table", "thead", "tbody", "tfoot", "tr", "td", "th"};
    std::string tok;
    for (size_t i = 0; i <= selector.size(); ++i) {
        char c = (i < selector.size()) ? selector[i] : '\0';
        if (std::isalnum(static_cast<unsigned char>(c)) || c == '-' || c == '_') {
            tok.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(c))));
        } else {
            if (!tok.empty() && kTags.count(tok)) return true;
            tok.clear();
        }
    }
    return false;
}

// Collects every rule in @p sheet whose selector references either a
// table-structural tag (see selectorReferencesTableStructure) or one of
// @p usedClasses, serialized verbatim in document order. This is
// deliberately conservative/over-inclusive rather than trying to fully
// parse and evaluate compound/descendant CSS selectors (e.g. "thead
// th.eigen .sub"): a rule that textually references relevant structure
// gets included even if some other part of its selector wouldn't actually
// match here, since an inapplicable rule is inert CSS, while a dropped one
// is exactly the "table background/highlighting silently lost" bug this
// exists to fix.
std::string collectTableStructureCss(const libhtmlpp::CSSStyleSheet &sheet,
                                      const std::set<std::string> &usedClasses)
{
    std::string css;
    for (const auto &rule : sheet.getRules()) {
        const std::string &sel = rule.getSelector();
        if (sel.empty() || sel[0] == '@') continue;

        bool relevant = selectorReferencesTableStructure(sel);
        if (!relevant) {
            for (const auto &cls : usedClasses) {
                if (selectorReferencesClass(sel, cls)) { relevant = true; break; }
            }
        }
        if (relevant) {
            css += rule.serialize(true);
            css += "\n";
        }
    }
    return css;
}

// 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
@@ -1077,13 +1187,25 @@ void blogi::htmlimport::htmlElementToWidgetXml(

                struct RowData {
                    std::vector<std::string> cells;
                    std::vector<std::string> cellClasses;
                    std::string rowClass;
                    bool isHeader;
                };
                std::vector<RowData> rows;

                // Every class name seen on a <tr>/<td>/<th> or nested inside
                // a cell's inner HTML -- fed to collectTableStructureCss
                // below so the CSS rules that style them (e.g. ".eigen",
                // ".ja") make it into the Table's CustomCss instead of being
                // silently dropped (table cells don't go through the normal
                // per-element traversal that would otherwise pick these up).
                std::set<std::string> usedClasses;

                auto processTr = [&](libhtmlpp::HtmlElement *tr, bool header) {
                    RowData rd;
                    rd.isHeader = header;
                    rd.rowClass = tr->getAtributte("class");
                    if (!rd.rowClass.empty()) collectClassTokens(rd.rowClass, usedClasses);
                    libhtmlpp::Element *tdEl = tr->firstChild();
                    while (tdEl) {
                        if (tdEl->getType() == libhtmlpp::HtmlEl) {
@@ -1093,7 +1215,12 @@ void blogi::htmlimport::htmlElementToWidgetXml(
                            std::transform(tdTag.begin(), tdTag.end(), tdTag.begin(),
                                [](unsigned char c) { return std::tolower(c); });
                            if (tdTag == "td" || tdTag == "th") {
                                rd.cells.push_back(serializeInnerHtml(td));
                                std::string cellHtml = serializeInnerHtml(td);
                                std::string cellClass = td->getAtributte("class");
                                if (!cellClass.empty()) collectClassTokens(cellClass, usedClasses);
                                scanHtmlForClasses(cellHtml, usedClasses);
                                rd.cells.push_back(cellHtml);
                                rd.cellClasses.push_back(cellClass);
                            }
                        }
                        tdEl = tdEl->nextElement();
@@ -1157,11 +1284,15 @@ void blogi::htmlimport::htmlElementToWidgetXml(
                for (const auto &rd : rows) {
                    tinyxml2::XMLElement *rowEl = doc.NewElement("Row");
                    rowEl->SetAttribute("index", row);
                    if (!rd.rowClass.empty())
                        rowEl->SetAttribute("css_class", rd.rowClass.c_str());
                    int col = 0;
                    for (const auto &cellText : rd.cells) {
                    for (size_t i = 0; i < rd.cells.size(); ++i) {
                        tinyxml2::XMLElement *cellEl = doc.NewElement("Cell");
                        cellEl->SetAttribute("col", col);
                        cellEl->SetText(cellText.c_str());
                        if (!rd.cellClasses[i].empty())
                            cellEl->SetAttribute("css_class", rd.cellClasses[i].c_str());
                        cellEl->SetText(rd.cells[i].c_str());
                        rowEl->InsertEndChild(cellEl);
                        col++;
                    }
@@ -1173,6 +1304,11 @@ void blogi::htmlimport::htmlElementToWidgetXml(
                    "border","background-color","border-collapse","width","height",
                    "display","visibility"};
                std::string custom = extractCustomCss(cssProps, knownCss, mediaRules, cssClass, elemId);
                std::string structureCss = collectTableStructureCss(sheet, usedClasses);
                if (!structureCss.empty()) {
                    if (!custom.empty()) custom += " ";
                    custom += structureCss;
                }
                if (!custom.empty()) {
                    tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss");
                    ccEl->SetText(custom.c_str());