Loading editor/src/htmlimport.cpp +139 −3 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) { Loading @@ -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(); Loading Loading @@ -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++; } Loading @@ -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()); Loading Loading
editor/src/htmlimport.cpp +139 −3 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) { Loading @@ -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(); Loading Loading @@ -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++; } Loading @@ -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()); Loading