#include "grim.h" #include #include #include #include #include #include #include #include #include #include #include // +------------------------------------------------------+ // |(TOC) Table of contents | // +------------------------------------------------------+ // |(p98ym) Utils | // | (hldfk) Document Parsing | // |(tigfn) Attribute Handling | // |(f98y0) ClassList | // |(lhujn) StyleList | // |(kuib7) Other Node functions | // |(mnjki) CSS Style Handling | // |(jhsfk) Has selection | // |(d678a) Nth child selection | // +------------------------------------------------------+ // +---------------------------------------------------+ // | (p98ym) Utils | // +---------------------------------------------------+ void trimSpace(std::string& str) { if (str.empty()) { return; } // Find first non-space size_t first_non_space = 0; while (first_non_space < str.length() && std::isspace(str[first_non_space])) { first_non_space++; } // If all spaces or empty if (first_non_space == str.length()) { str.clear(); // Set to empty string return; } // Find last non-space size_t last_non_space = str.length() - 1; while (last_non_space >= first_non_space && std::isspace(str[last_non_space])) { last_non_space--; } // Erase trailing spaces if (last_non_space < str.length() - 1) { str.erase(last_non_space + 1); } // Erase leading spaces if (first_non_space > 0) { str.erase(0, first_non_space); } } // +---------------------------------------------------+ // | (hldfk) Document Parsing | // +---------------------------------------------------+ // Node* CreateDocument(std::string path) { // Parse document // Pull links from head and get stylesheets // parse css (pass window to precalculate all of the values) // } // +---------------------------------------------------+ // | (tigfn) Attribute Handling | // +---------------------------------------------------+ // In the grim engine everything except the tagname of // an node is considered a string stored attribute. To // get the value of an attribute in its desired type the // attribute getter function uses implicit conversion // operators. The types that are supported are double, // bool, int, std::string. There are special attribute // handlers like ClassList (.classList) and StyleList // (.style) for more information on each view // f98y0 and lhujn respectively. // The Attribute class is defined in the header file static const std::unordered_map DEFAULT_ATTRIBUTE_VALUES = { {"tabindex", "-1"}, {"hidden", "false"} }; // Tag Name is not a attribute so it needs to be handled differently std::string Node::getTagName() const { return TagName; } void Node::setTagName(const std::string& name) { TagName = name; } void Node::setAttribute(const std::string& name, const std::string& value) { attributes[name] = value; } std::optional Node::getAttribute(const std::string& name) const { // First check in the user set attributes auto it = attributes.find(name); if (it != attributes.end()) { // return the value wrapped in the Attribute class return Attribute(it->second); } else { // If its not set by the user then find it in the // default values map it = DEFAULT_ATTRIBUTE_VALUES.find(name); if (it != DEFAULT_ATTRIBUTE_VALUES.end()) { return Attribute(it->second); } else { return std::nullopt; } } } std::vector Node::getAttributeKeys() { std::vector keys; for(auto p : attributes) { keys.push_back(p.first); } return keys; } const std::unordered_map& Node::getAttributes() const { return attributes; } // +---------------------------------------------------+ // | (f98y0) ClassList | // +---------------------------------------------------+ void Node::ClassList::createIndex() { int index = 0; // Add a space to the back to help trigger the adding of the // last item std::string classes = self->attributes["class"]+" "; // Update the stored length len = classes.length(); indexes.clear(); bool waitForFirstChar = true; for (size_t i = 0; i < len; i++) { if (waitForFirstChar && classes[i] != ' ') { index = i; waitForFirstChar = false; } else if (!waitForFirstChar) { if (classes[i] == ' ') { indexes.push_back({index, i-index}); waitForFirstChar = true; } } } } bool Node::ClassList::checkIndex() { std::string classes = self->attributes["class"]; if (classes.length() != len) { return false; } else { // Skip the first one because there is a space // also we don't care if the values are the same // just that the array indexing is the same. If the // value is the same size and position it doesn't matter for (size_t i = 1; i < indexes.size(); i++) { // See if the index position is a space // if not return false if (classes[indexes[i-1].first] != ' ') { return false; } } return true; } } size_t Node::ClassList::length() { if (!checkIndex()) { createIndex(); } return indexes.size(); } std::string Node::ClassList::item(size_t key) { if (!checkIndex()) { createIndex(); } if (key < indexes.size()) { std::pair pair = indexes[key]; return self->attributes["class"].substr(pair.first, pair.second); } else { return ""; } } void Node::ClassList::add(std::string value) { if (!checkIndex()) { createIndex(); } if (self->attributes["class"].length() == 0) { self->attributes["class"] += value; } else { self->attributes["class"] += " "+value; } // len because the index of the added space will be the length of // the prevous string int vl = value.length(); indexes.push_back({len, vl}); len += vl+1; } void Node::ClassList::remove(std::string value) { if (!checkIndex()) { createIndex(); } int newLen = value.length(); size_t cLen = indexes.size(); std::string classes = self->attributes["class"]; for (size_t i = 0; i < cLen; i++) { std::pair pair = indexes[i]; if (newLen == pair.second) { if (classes.substr(pair.first, pair.second) == value) { // Splice out the value to be removed // ISSUE: This will keep the surounding spaces and cause the size to grow when // classes are added and removed self->attributes["class"] = classes.substr(0, pair.first)+classes.substr(pair.first+pair.second); } } } } bool Node::ClassList::contains(std::string value) { if (!checkIndex()) { createIndex(); } int newLen = value.length(); size_t cLen = indexes.size(); std::string classes = self->attributes["class"]; bool found = false; for (size_t i = 0; i < cLen; i++) { std::pair pair = indexes[i]; if (newLen == pair.second) { if (classes.substr(pair.first, pair.second) == value) { // Same logic as remove but turns found true then breaks found = true; break; } } } return found; } void Node::ClassList::toggle(std::string value) { if (contains(value)) { remove(value); } else { add(value); } } // +---------------------------------------------------+ // | (lhujn) StyleList | // +---------------------------------------------------+ // StyleList provides the JS .style API with a few changes // to get/set values use .get or .set. To get the entire // value call .getAttribute("style"). The StyleList stores // a index of the positions of the first charactor, the ':', // and the position of the ';'. On every read/write the index // is verified then the change or read is made. If the value // changes the positions of the index it will be updated on the // next read/write. This allows the attribute "style" to always // contain the true value of the inline style and still have an // easy to use API for .style. To get all styles, call .length() // and itterate through all items using .item(). void Node::StyleList::createIndex() { std::string styles = self->attributes["style"]; // Update the stored length len = styles.length(); indexes.clear(); // " background-color: white;" // ^ int firstLetter = 0; bool firstNonWhiteSpace = false; // " background-color: white;" // ^ int splitPoint = 0; bool keyFound = false; // " background-color: white;" // ^ int end = 0; bool secondNonWhiteSpace = false; // " background-color: white;" // ^ // Reset all to false and add the indexs for (size_t i = 0; i < len; i++) { char s = styles[i]; if (firstNonWhiteSpace) { if (keyFound) { if (secondNonWhiteSpace) { if (s == ';' || i >= len-1) { if (i >= len-1 && s != ';') { end = i+1; } else { end = i; } indexes.push_back({firstLetter, splitPoint, end}); firstNonWhiteSpace = false; secondNonWhiteSpace = false; keyFound = false; } } else if (!std::isspace(s)) { secondNonWhiteSpace = true; } } else if (s == ':') { splitPoint = i; keyFound = true; } } else if (!std::isspace(s)) { firstLetter = i; firstNonWhiteSpace = true; } } } bool Node::StyleList::checkIndex() { std::string styles = self->attributes["style"]; if (styles.length() != len) { return false; } else { for (size_t i = 0; i < indexes.size(); i++) { if (std::isspace(styles[std::get<0>(indexes[i])]) || styles[std::get<1>(indexes[i])] != ':' || styles[std::get<2>(indexes[i])] != ';' ) { return false; } } return true; } } std::pair Node::StyleList::item(size_t index) { if (!checkIndex()) { createIndex(); } std::pair property; std::string styles = self->attributes["style"]; int f = std::get<0>(indexes[index]); int s = std::get<1>(indexes[index]); property.first = styles.substr(f, s-f); trimSpace(property.first); int t = std::get<2>(indexes[index]); property.second = styles.substr(s+1, t-(s+1)); trimSpace(property.second); return property; } size_t Node::StyleList::length() { if (!checkIndex()) { createIndex(); } return indexes.size(); } std::string Node::StyleList::get(std::string key) { if (!checkIndex()) { createIndex(); } std::string styles = self->attributes["style"]; for (size_t i = 0; i < indexes.size(); i++) { int f = std::get<0>(indexes[i]); int s = std::get<1>(indexes[i]); std::string slicedKey = styles.substr(f, s-f); trimSpace(slicedKey); if (slicedKey == key) { int t = std::get<2>(indexes[i]); std::string value = styles.substr(s+1, t-(s+1)); trimSpace(value); return value; } } return ""; } void Node::StyleList::set(std::string key, std::string value) { if (!checkIndex()) { createIndex(); } std::string styles = self->attributes["style"]; for (size_t i = 0; i < indexes.size(); i++) { int f = std::get<0>(indexes[i]); int s = std::get<1>(indexes[i]); std::string slicedKey = styles.substr(f, s-f); trimSpace(slicedKey); if (slicedKey == key) { int t = std::get<2>(indexes[i]); self->attributes["style"] = styles.substr(0, f) +" "+key+":"+value+";"+styles.substr(t); // Return early to skip the appending value; return; } } // If the property wasn't replaced then add it char lastChar = styles[std::get<2>(indexes[indexes.size()-1])]; if (lastChar == ';') { self->attributes["style"] += " "+key+":"+value+";"; } else { // If the user didn't terminate the last style // terminate it for them self->attributes["style"] += "; "+key+":"+value+";"; } } void Node::StyleList::remove(std::string key) { if (!checkIndex()) { createIndex(); } std::string styles = self->attributes["style"]; for (size_t i = 0; i < indexes.size(); i++) { int f = std::get<0>(indexes[i]); int s = std::get<1>(indexes[i]); std::string slicedKey = styles.substr(f, s-f); trimSpace(slicedKey); if (slicedKey == key) { int t = std::get<2>(indexes[i]); self->attributes["style"] = styles.substr(0, f) + styles.substr(t); return; } } } // +---------------------------------------------------+ // | (kuib7) Other Node functions | // +---------------------------------------------------+ Node* Node::createElement(std::string name) { std::unique_ptr newNode = std::make_unique(); newNode->setTagName(name); newNode->parent = this; children.push_back(std::move(newNode)); return children.back().get(); } std::string Node::print(int indent) { std::string out = ""; for (int i = 0; i < indent; ++i) { out += " "; } out += "<" + getTagName(); for (const auto& attr_pair : getAttributes()) { if (attr_pair.first == "innerText" || attr_pair.first == "tagName") { continue; } out += " " + attr_pair.first; if (!attr_pair.second.empty()) { out += "=\"" + attr_pair.second + "\""; } } out += ">"; if (auto innerText = getAttribute("innerText")) { if (innerText != std::nullopt) { out += "\n" + *innerText->String(); } } out += "\n"; for (const auto& child : children) { out += child->print(indent + 1)+"\n"; } for (int i = 0; i < indent; ++i) { out += " "; } out += "\n"; return out; } // +---------------------------------------------------+ // | (mnjki) CSS Style Handling | // +---------------------------------------------------+ // | (78658) Selector Parsing | // | (luoiy) Node Selector Parsing | // +---------------------------------------------------+ // +---------------------------------------------------+ // | (78658) Selector Parsing | // +---------------------------------------------------+ // parseSelectorParts deconstructs a selector into its indiviual parts // so they can be store and used in things like finding styles for basemap // and comparing a node to a selector in testSelector. This is a higher level function // that should be ran once parselector and the results saved somewhere due to // the high cost to run the function std::vector> parseSelectorParts(std::string_view selector) { // need to account for selectors with parenthesis like: h1:(+ input:required) // need to convert all single quotes to double quotes // need to account for commas, will return // need to colapse all spaces to a single space // need to trim spaces std::vector> parts; std::vector buffer; std::string current; size_t nesting = 0; size_t sl = selector.length(); current.reserve(sl); for (size_t e = 0; e < sl; e++) { char s = selector[e]; if (s == '\'') { // convert single quotes to double quotes s = '\"'; } if (s == ' ' && nesting == 0) { if (e > 0 && e < sl-1) { bool prevMatch = selector[e-1] == '>' || selector[e-1] == '+' || selector[e-1] == '~'; bool nextMatch = selector[e+1] == '>' || selector[e+1] == '+' || selector[e+1] == '~'; if (prevMatch && !nextMatch) { s = '\0'; } else if (!prevMatch && nextMatch) { s = '\0'; } } } if (nesting == 0 ) { if (s == ':' || s == '[' || s == ',' || s == '#' || s == '.') { // We convert any : selectors (like :checked) to [checked] // because we store every thing as attributes on the Node // when test selector is ran it will convert all its attributes // to [checked] or [href="url"] // so we can easily compare the two if (std::isspace(current.front()) || std::isspace(current.back())) { trimSpace(current); } if (!current.empty()) { if (current[0] == ':' && current.back() != ')' && current.length() > 1 && current[1] != ':') { current[0] = '['; current.push_back(']'); } buffer.push_back(std::move(current)); } if (s == ':' && e+1 < sl && selector[e+1] == ':') { current.clear(); current = "::"; e++; continue; } else { current = s; } } else if (s == '>' || s == '+' || s == '~' || s == ' ') { if (std::isspace(current.front()) || std::isspace(current.back())) { trimSpace(current); } if (!current.empty()) { if (current[0] == ':' && current.back() != ')' && current.length() > 1 && current[1] != ':') { current[0] = '['; current.push_back(']'); } buffer.push_back(std::move(current)); buffer.push_back(""); buffer.back() += s; } else if (s != ' ') { buffer.push_back(""); buffer.back() += s; } current.clear(); } else if (s != '\0') { current.push_back(s); } } else if (s != '\0') { current.push_back(s); } if ((s == ',' && nesting == 0) || e == sl-1) { if (std::isspace(current.front()) || std::isspace(current.back())) { trimSpace(current); }; if (!current.empty() && current != ",") { if (current[0] == ':' && current.back() != ')' && current.length() > 1 && current[1] != ':') { current[0] = '['; current.push_back(']'); } buffer.push_back(std::move(current)); } parts.push_back(std::move(buffer)); buffer.clear(); current.clear(); } if (s == '(' || s == '[' || s == '{') { nesting++; } else if (s == ')' || s == ']' || s == '}') { nesting--; } } return parts; } // +---------------------------------------------------+ // | (luoiy) Node Selector Parsing | // +---------------------------------------------------+ // std::vector StyleHandler::parseNodeParts(Node* node) { // This isn't a all inclusive list of parts that can // apply to the node but it narrows our search for matching // classes down quite a bit std::vector parts; parts.push_back(node->getTagName()); auto id = node->getAttribute("id"); if (id != std::nullopt) { parts.push_back("#"+*id->String()); } // classes and attributes auto keys = node->getAttributeKeys(); for (auto k : keys) { if (k != "tagName" || k != "id" || k != "innerText" || k != "class") { std::string v; auto av = node->getAttribute(k); if (av != std::nullopt) { v = "=\""+*av->String()+"\""; } parts.push_back('['+k+v+']'); } } auto classLen = node->classList.length(); for (size_t i = 0; i < classLen; i++) { parts.push_back("."+node->classList.item(i)); } return parts; } /* void Sheet::add(std::string selector, std::unordered_map properties) { // Type is 2d vector of selector parts auto parts = parseSelectorParts(selector); for (auto pr : parts) { for (auto p : pr) { std::cout << p << std::endl; } } size_t index = styles.size(); styles.push_back({parts,properties}); // Get the last selector group from the parsed selector parts // because that is the selector that applies to an element // and map them into basemap std::vector targetParts = parts[parts.size()-1]; for (auto p : targetParts) { if (basemap.find(p) != basemap.end()) { basemap[p].push_back(index); } else { std::vector bm = {index}; basemap[p] = bm; } } } */ void getstyle(std::vector> parts, Sheet* sheet, std::unordered_map* styles) { std::vector canidates; // Now we have to start from the window.CSS.root, use the basemap to find styles // | > include a search for "*" // | test each selector, if a selector matches, go into its children and repeat for (auto p : parts) { if (auto sty = sheet->basemap.find(p); sty != sheet->basemap.end()) { for (auto s : sty->second) { bool found = false; // This makes sure we aren't adding the same styles to the candidates list for (auto c : canidates) { if (c == s) { found = true; break; } } if (!found) { canidates.push_back(s); } } } } // Now we have the list of candidates next we will refined our selection // we need to pull the selectors from each candidate then run testSelector on it for (size_t c : canidates) { // need to check the type of sheet it is SheetType t = sheet->type; std::vector> selector = sheet->children[c].selector; if (t == SheetType::STYLE) { // Run testSelector with the preparsed parts if (testSelector(node, selector)) { std::unordered_map props = sheet->children[c].properties; for (auto p : props) { styles[p.first] = p.second; } // If the Sheet matches then we will repeat the same process with that sheet if (sheet->children[c].children.size() > 0) { getstyle(parts, sheet->children[c], styles); } } } else if (t == SheetType::CONTAINER) { } } } // TODO:Add variable subsutution and inheritance, fill in the & in nested std::unordered_map StyleHandler::getStyles(Node* node) { // TODO: Will need to change getStyles to first consider if the style sheet applies // then to check the selectors std::vector parts = parseNodeParts(node); std::unordered_map styles; getstyles(parts, window.CSS.root, &styles); // Now we have all styles except the inline styles and the inherited styles } Style StyleHandler::item(int index) { return styles[index]; } // Used for root node without a parent const std::vector> EMPTY_NODE_CHILDREN_VECTOR; add: & subs add: testing from inheritance at-rules need to be calculated at getstyles make a function to testAtRules // node: The node you are checking the selector against // selector: output of parseSelector bool testSelector(Node* node, std::vector> selector) { // By the time we get here, we know the right most selector some what matches // so now if it is a compound selector we should see if the parent('s) some what match if (selector.size() > 1) { // If a selector is made up of several different selectors // ie {{"h1"}, {"h2"}, {"h3"}} // run each selector and return on a match for (auto s : selector) { bool match = testSelector(node, {s}); if (match) { return true; } } return false; } std::string tagName = node->getTagName(); std::vector trimmed; std::vector parts; std::string symbol; bool symbolFound = false; // This is setup code for the rest of the process // parts is the part of the selector that applies to // the current node: If we had div > h1 we would get // "h1" and the div part would be stored in trimmed. // then the symbol we split at is symbol. See below for (int i = selector[0].size()-1; i >= 0; i--) { std::string part = selector[0][i]; if (symbolFound) { trimmed.insert(trimmed.begin(), part); } else { if (part != ">" && part != " " && part != "+" && part != "~") { parts.insert(parts.begin(), part); } else { symbol = part; symbolFound = true; } } } // Given our selector is: {{"div", ">", "h1"}} // we should now have // trimmed = {"div"} // parts = {"h1"} // symbol = ">" // matched is true here because the node matching logic // returns if matched is false bool matched = true; // Now we have prepped the selector, we need to get the // information for the node. We need to store them in variables // then loop through the parts to test them because at this // point we don't know what type of selector each part is. // Possibly in the future it could be reworked and we could // do the match detecting while adding. This would let us // return at the earliest possible part avoiding unnessisary // computing std::vector nodeClasses; size_t classLen = node->classList.length(); for (size_t i = 0; i < classLen; i++) { nodeClasses.push_back(node->classList.item(i)); } std::unordered_map attributes = node->getAttributes(); std::vector attrs; for (auto a : attributes) { if (a.second != "") { attrs.push_back(a.first+"=\""+a.second+"\""); } else { attrs.push_back(a.first); } } // Find the index of the child relative to it's parent // the children variable is the PARENTS CHILDREN the the current node's children // if you want those, node->children const std::vector>& children = (node->parent != nullptr) ? node->parent->children : EMPTY_NODE_CHILDREN_VECTOR; size_t childIndex = 0; size_t childTypeIndex = 0; size_t childTypeCount = 0; if (children.size() > 1) { for (size_t i = 0; i < children.size(); i++) { Node* child_ptr = children[i].get(); if (child_ptr == node) { childIndex = i; childTypeIndex = childTypeCount; } if (child_ptr->getTagName() == tagName) { childTypeCount++; } } } // Add the auto pseudo classes like first-child/last-child if (childIndex == 0) { attrs.push_back("first-child"); } if (childIndex == children.size()-1) { attrs.push_back("last-child"); } if (childTypeIndex == 0) { attrs.push_back("first-of-type"); } if (childTypeIndex == childTypeCount-1) { attrs.push_back("last-of-type"); } if (childIndex == 0 && children.size() == 1) { attrs.push_back("only-child"); } if (childTypeIndex == 0 && childTypeCount == 1) { attrs.push_back("only-of-type"); } if (tagName == "html") { attrs.push_back("root"); } if (tagName == "input") { std::string type = attributes["type"]; bool readonly = attributes.find("readonly") != attributes.end(); bool disabled = attributes.find("disabled") != attributes.end(); if (type == "text" || type == "search" || type == "tel" || type == "url" || type == "email" || type == "password" || type == "date" || type == "month" || type == "week" || type == "time" || type == "datetime-local" || type == "number") { if (readonly || disabled) { attrs.push_back("read-only"); } else { attrs.push_back("read-write"); } } else { attrs.push_back("read-only"); } } else if (tagName == "textarea") { bool readonly = attributes.find("readonly") != attributes.end(); bool disabled = attributes.find("disabled") != attributes.end(); if (readonly || disabled) { attrs.push_back("read-only"); } else { attrs.push_back("read-write"); } } else { std::string contentEditable = attributes["contenteditable"]; if (contentEditable == "true") { attrs.push_back("read-write"); } else { attrs.push_back("read-only"); } } size_t i = 0; for (auto p : parts) { if (p[0] == '*') { matched = true; } else if (i == 0 && p[0] != '#' && p[0] != '.' && p[0] != '[' && p[0] != ':') { matched = tagName == p; } else if (p[0] == '#') { matched = "#"+attributes["id"] == p; } else if (p[0] == '.') { bool found = false; p.erase(0,1); for (auto c : nodeClasses) { if (p == c) { found = true; break; } } matched = found; } else if (p[0] == '[') { // This covers all attributes and non relative pseudo-classes // as parseSelectorParts converts any attribute // without a ending ) to [name]: // See tests/css_selector.cc for examples bool found = false; p.erase(0,1); p.erase(p.length()-1,1); for (auto a : attrs) { if (p == a) { found = true; break; } } matched = found; } // !TODO: Need a case for : , this will run the :has... logic else if (p[0] == ':') { if (p.starts_with(":is(")) { std::string value = p.substr(4,p.length()-5); matched = testSelector(node, parseSelectorParts(value)); } else if (p.starts_with(":where(")) { // Per specification where and is differ in specificity only // however, grim does not support specificity // https://developer.mozilla.org/en-US/docs/Web/CSS/:is#difference_between_is_and_where std::string value = p.substr(7,p.length()-8); matched = testSelector(node, parseSelectorParts(value)); } else if (p.starts_with(":not(")) { std::string value = p.substr(5,p.length()-6); // Just inverts the output of testSelector matched = !testSelector(node, parseSelectorParts(value)); } else if (p.starts_with(":has(")) { // +------------------------------------------------------+ // |(jhsfk) Has selection | // +------------------------------------------------------+ // We have to do a little work to get :has working // once we parse the inner parts of the selector, // we need to find what the selector is targeting std::string value = p.substr(5,p.length()-6); auto innerParts = parseSelectorParts(value); bool found = false; for (auto ip : innerParts) { // if the selector was :has(+ p, div, > video) // ip contains each comma seperated value like {"+", "p"} // these differ from how we parse the parts below // if the first item in a selector is not a +~> then // it is a decendent otherwise they are the same // We need to have a 2d vector with the contents of ip without the first // selector if it is't a decendent combinator, so we do that here std::vector sliceOfParts(ip.begin()+1, ip.end()); std::vector> slicedParts; slicedParts.push_back(sliceOfParts); if (ip[0] == ">") { // The child combinator only checks direct children so we // can loop through and if we find one that matches ip[1:] // (remove the ">") we can stop and return for (const auto& c : node->children) { found = testSelector(c.get(), slicedParts); if (found) { break; } } } else if (ip[0] == "+") { if (children.size() > 1) { // If there isn't a prevous sibling then it cannot match if (childIndex < children.size()-1) { // In this selector we just need to get the direct sibling and test it found = testSelector(children[childIndex+1].get(), slicedParts); } else { // if the element is the last there is no way it could have one found = false; } } } else if (ip[0] == "~") { if (childIndex < children.size()-1) { for (size_t i = childIndex+1; i < children.size(); i++) { // We have to test every selector after the current element, // if any match then it matches found = testSelector(children[i].get(), slicedParts); if (found) { break; } } } else { found = false; } } else { // No combinator default to decendent // Sadly we can't use the check parent trick here // as we do below. We need to BFS the children until we find a match std::queue queue; std::set visited; queue.push(node); visited.insert(node); while (!queue.empty()) { Node* n = queue.front(); queue.pop(); std::vector> ipp; ipp.push_back(ip); if (testSelector(n, ipp)) { found = true; break; } for (const auto& child_ptr : n->children) { Node* child = child_ptr.get(); if (visited.find(child) == visited.end()) { visited.insert(child); queue.push(child); } } } } if (found) { break; } } matched = found; } else if (p.starts_with(":nth-")) { // +------------------------------------------------------+ // |(d678a) Nth child selection | // +------------------------------------------------------+ // This will match on: // | nth-child() // | nth-last-child() // | nth-last-of_type() // | nth-of-type() // Instead of moving these up a layer I'm putting them // here because most selectors will not be "nth-" so why // check against every case std::string pattern; bool last = false; bool ofType = false; if (p.starts_with(":nth-child")) { pattern = p.substr(11, p.length()-12); } else if (p.starts_with(":nth-last-child(")) { last = true; pattern = p.substr(16,p.length()-17); } else if (p.starts_with(":nth-of-type(")) { ofType = true; pattern = p.substr(13,p.length()-14); } else if (p.starts_with(":nth-last-of-type(")) { ofType = true; last = true; pattern = p.substr(18, p.length()-19); } // +------------------------------------------------------+ // |Parsing the pattern | // +------------------------------------------------------+ // If you look closely these are reversed // its because the vector index starts with 0 // and CSS starts with 1 if (pattern == "even") { if (childIndex%2 == 1) { matched = true; } else { matched = false; } } else if (pattern == "odd") { if (childIndex%2 == 0) { matched = true; } else { matched = false; } } else { // loop through // first check if theres a symbol before any 0-9 // go until n is found (if there is a n) // then find another symbol or "of" // if symbol find numbers // then of get that. // the of syntax can be used to make nth-of-type as well bool negFound = false; bool negative = false; bool Afound = false; bool nfound = false; int A = 0; int B = 0; char symbol = '+'; bool symbolFound = false; std::string ofSelector = ""; for (size_t i = 0; i < pattern.length(); i++) { if (!std::isspace(pattern[i])) { if (pattern[i] == '-' && !negFound) { negFound = true; negative = true; } else if (pattern[i] == '+' && !negFound) { negFound = true; } else if (pattern[i] == 'n' && !nfound && !Afound ) { nfound = true; // At n stop adding to A Afound = true; negFound = true; } else if (Afound && nfound && !symbolFound && (pattern[i] == '-' || pattern[i] == '+')) { symbol = pattern[i]; symbolFound = true; } else if (pattern[i]-'0' < 10) { negFound = true; if (!Afound && !nfound) { A *= 10; A += (pattern[i]-'0'); } else if (nfound && symbolFound && Afound) { B *= 10; B += (pattern[i]-'0'); } } else { ofSelector += pattern[i]; } } } if (negative) { A *= -1; } if (symbol == '-') { B *= -1; } // Now we have: // polarity of A: negative // value of A: A // polarity of B: symbol // value of B: B // value of the of selector: ofSelector.substr(2) ("of" is included in the value of ofSelector) // // ^ We have already applied the polarities so we just need A,B and ofSelector // // Things to keep in mind: // if there is a A value it will be 0 // +------------------------------------------------------+ // |Finding the index of node | // +------------------------------------------------------+ int index = 0; // We have the index of the current element (childIndex) // if ofSelector is "" we can use it if not we need to run // testSelector on all children until we find it // and if "last" we need to do it in reverse // // If the nth child selector has -of-type we need to add of+tagname // the of is added because thats how the parser parses the rest of them if (ofType) { ofSelector = "of"+tagName; } if (ofSelector == "") { if (last) { // If we are checking from the back we need to invert the // childIndex index = children.size() - 1 - childIndex; } else { index = childIndex; } } else { // Parse the of selector auto pOS = parseSelectorParts(ofSelector.substr(2)); if (testSelector(node, pOS)) { if (last) { // Reverse through the siblings for (size_t i = children.size() - 1; i >= 0; i--) { Node* child_ptr = children[i].get(); if (child_ptr != node) { if (testSelector(child_ptr, pOS)) { // if the selector matches the ofSelector add one index++; } } else { // We are at the pointer address of the target node break; } } } else { for (size_t i = 0; i < children.size(); i++) { Node* child_ptr = children[i].get(); if (child_ptr != node) { if (testSelector(child_ptr, pOS)) { // if the selector matches the ofSelector add one index++; } } else { // We are at the pointer address of the target node break; } } } } else { // the target node doesn't match the of selector then it can not // be a match so return false no need to set matched to false and continue return false; } } // Add one to index because CSS starts at 1 index++; // Now we have the index of the node and we can compute if its a match // this is done by calculating the An+B // The An+B is calculated based on two things // n must be >= 0 // n cannot be a float // so we solve for n with (index-B)/A then we check those two conditions // if nfound is false and A != 0 then we can just compare the two numbers // Given n+B A is implicitly 1, this will fail if A is set to 0 if (A == 0) { A = 1; } if (!nfound) { matched = A == index; } else { matched = (index-B)/A >= 0 && (index-B)%A == 0; } } } // If there are any pseudo elements we will just skip them // with the transformers being replaced with pseudo elements // we won't know all of them. Will need to have a function to strip // the element names } if (!matched) { // Return out of the function return false; } i++; } // if the node did not match it would have returned false so if there isn't // a symbol then we return true if (symbol == "") { return true; } // The rest of the logic assumes the other selectors do not match until proven otherwise matched = false; if (node->parent == nullptr) { return false; } if (symbol == ">") { // child combinator // We are just testing the parent node here matched = testSelector(node->parent, {trimmed}); } else if (symbol == "+") { // next sibling combinator if (children.size() > 1) { // If there isn't a prevous sibling then it cannot match if (childIndex == 0) { return false; } else { // In this selector we just need to get the direct sibling and test it matched = testSelector(children[childIndex-1].get(), {trimmed}); } } } else if (symbol == "~") { // Subsequent-sibling combinator if (children.size() > 1) { for (size_t i = 0; i < childIndex; i++) { // We have to test every selector before the current element, // if any match then it matches matched = testSelector(children[i].get(), {trimmed}); if (matched) { break; } } } } else if (symbol == " ") { // For the descendant combinator we have to check every parent until we match or nullptr out Node* current = node->parent; bool hasParent = true; while (hasParent) { if (testSelector(current, {trimmed})) { matched = true; break; } if (current->parent != nullptr) { current = current->parent; } else { hasParent = false; } } } return matched; }