package grim import ( "strconv" "strings" ) // !TODO: Make var() and :root (root is implide) // !MAN,ELEMENT: QuerySelector is a Node method to select an element using a CSS selector // + [!MAN]Note: the CSS query is performed relative to the Node calling it // + [!MAN]Usage: element.Node.QuerySelector("css,query") -> element.Node // + [!DEVMAN]Note: See TestSelector for query information func (n *Node) QuerySelector(selectString string) *Node { m, _ := TestSelector(n, selectString) if m { return n } for i := range n.Children { el := n.Children[i] cr := el.QuerySelector(selectString) if cr.Properties.Id != "" { return cr } } return &Node{} } // !MAN,ELEMENT: QuerySelectorAll is a Node method to select all matching elements using a CSS selector // + [!MAN]Note: the CSS query is performed relative to the Node calling it // + [!MAN]Usage: element.Node.QuerySelectorAll("css,query") -> []element.Node // + [!DEVMAN]Note: See TestSelector for query information func (n *Node) QuerySelectorAll(selectString string) *[]*Node { results := []*Node{} m, _ := TestSelector(n, selectString) if m { results = append(results, n) } for i := range n.Children { el := n.Children[i] cr := el.QuerySelectorAll(selectString) if len(*cr) > 0 { results = append(results, *cr...) } } return &results } type SelectorParts struct { TagName string Id string ClassList []string Attribute map[string]string } func splitSelector(selector string, key rune) []string { var parts []string var current strings.Builder nesting := 0 for _, r := range selector { switch r { case '(': nesting++ current.WriteRune(r) case ')': if nesting > 0 { nesting-- } current.WriteRune(r) case '[': nesting++ current.WriteRune(r) case ']': if nesting > 0 { nesting-- } current.WriteRune(r) case '{': nesting++ current.WriteRune(r) case '}': if nesting > 0 { nesting-- } current.WriteRune(r) case key: if nesting == 0 { // End of the current selector part parts = append(parts, strings.TrimSpace(current.String())) current.Reset() } else { // Inside nested context, add comma to the current part current.WriteRune(r) } default: current.WriteRune(r) } } // Add the last part if non-empty if current.Len() > 0 { parts = append(parts, strings.TrimSpace(current.String())) } return parts } // nthChildMatch checks if the given index matches the nth-child pattern. func NthChildMatch(pattern string, index int) bool { pattern = strings.ReplaceAll(pattern, " ", "") // Handle special cases for "odd" and "even" lowerPattern := strings.ToLower(strings.TrimSpace(pattern)) if lowerPattern == "odd" { return index%2 == 1 } if lowerPattern == "even" { return index%2 == 0 } // Coefficients for "an+b" a, b := 0, 0 nIndex := strings.Index(lowerPattern, "n") // Parse pattern with 'n' if nIndex != -1 { // Parse coefficient of "n" (before 'n') if nIndex == 0 || lowerPattern[0] == '+' { a = 1 } else if lowerPattern[0] == '-' && nIndex == 1 { a = -1 } else { var err error a, err = strconv.Atoi(lowerPattern[:nIndex]) if err != nil { return false } } // Parse constant term (after 'n') if nIndex+1 < len(lowerPattern) { b, _ = strconv.Atoi(lowerPattern[nIndex+1:]) } } else { // Handle single integer patterns like "3" var err error b, err = strconv.Atoi(lowerPattern) if err != nil { return false } } // Check if index matches the formula a*n + b if a == 0 { return index == b } return (index-b)%a == 0 && (index-b)/a >= 0 } // !ISSUE: :where doesn't work and h1+h1 is propagating to all elements on superselector // + :where selects properly but the margin doesn't... // + h1+h1 selects all but should only select the last two... func TestSelector(n *Node, selector string) (bool, bool) { selectors := splitSelector(selector, ',') if selector[0] == ':' && len(selectors) == 1 { if selector == ":" { return true, true } if len(selector) >= 5 && selector[0:5] == ":has(" { m := false for _, v := range n.Children { m, _ = TestSelector(v, selector[5:len(selector)-1]) if m { break } } return m, false } else if len(selector) >= 7 && selector[0:7] == ":where(" { m, _ := TestSelector(n, selector[7:len(selector)-1]) return m, false } else if len(selector) >= 4 && selector[0:4] == ":is(" { m, _ := TestSelector(n, selector[4:len(selector)-1]) return m, false } else if len(selector) >= 5 && selector[0:5] == ":not(" { m, _ := TestSelector(n, selector[5:len(selector)-1]) return !m, false } else if len(selector) >= 11 && selector[0:11] == ":nth-child(" { index := 0 if n.parent != nil { for _, v := range n.parent.Children { index++ if v.Properties.Id == n.Properties.Id { break } } } m := NthChildMatch(selector[11:len(selector)-1], index) return m, false } else if selector == ":required" { return n.required, false } else if selector == ":enabled" { return !n.disabled, false } else if selector == ":disabled" { return n.disabled, false } else if selector == ":checked" { return n.checked, false } else if selector == ":focus" { return n.focused, false } else if selector == ":hover" { return n.hovered, false } else if selector == ":before" { return true, true } else if selector == ":after" { return true, true } else { return false, false } } has, isPsuedo := false, false for _, s := range selectors { directChildren := splitSelector(s, '>') if len(directChildren) > 1 { currentElement := n match := true for i := len(directChildren) - 1; i >= 0; i-- { m, _ := TestSelector(currentElement, directChildren[i]) if !m { match = false } currentElement = currentElement.parent } has = match break } for _, dc := range directChildren { adjacentSiblings := splitSelector(dc, '+') if len(adjacentSiblings) > 1 { for i, v := range n.parent.Children { if v.Properties.Id == n.Properties.Id { // Make sure the current element matches the last selector m, _ := TestSelector(n, adjacentSiblings[len(adjacentSiblings)-1]) if i >= len(adjacentSiblings)-1 && m { match := true // Skip the first selector, its been matched to the current node adjIndex := len(adjacentSiblings) - 2 for j := i - 1; j >= i-len(adjacentSiblings)+1; j-- { sm, _ := TestSelector(n.parent.Children[j], adjacentSiblings[adjIndex]) if !sm { match = false break } adjIndex-- } has = match } break } } break } for _, as := range adjacentSiblings { generalSiblings := splitSelector(as, '~') if len(generalSiblings) > 1 { match := false for _, sel := range generalSiblings { for i := 0; i < len(n.parent.Children); i++ { v := n.parent.Children[i] m, _ := TestSelector(v, sel) if m { if match { has, isPsuedo = true, false break } match = true break } } if !match { break } } has = match break } for _, gs := range generalSiblings { descendants := splitSelector(gs, ' ') if len(descendants) > 1 { // !DEVMAN,ELEMENT,TESTSELECTOR: Descendants selector checks for any parent to have // + the next tag, but doesn't check the main tag if it did and the main didn't // + have the selector then it would move up. So instead the main tag is skipped // + until the next check. currentElement := n.parent match := false for i := len(descendants) - 1; i > 0; i-- { m := false for currentElement.parent != nil && !m { m, _ = TestSelector(currentElement, descendants[i]) if m { match = m break } currentElement = currentElement.parent } if !m { match = false break } } if !match { break } has = match // A break is not inserted here because of the main element check } for _, d := range descendants { computeAble := splitSelector(d, ':') if len(computeAble) == 0 { continue } baseNode := computeAble[0] if len(computeAble) > 1 { match := true for _, v := range computeAble[1:] { m, p := TestSelector(n, ":"+v) isPsuedo = p if !m { match = false } else if len(v) > 10 && v[0:10] == "nth-child(" { has = CompareSelector(baseNode, n) break } } // !ISSUE: Why is this here??? has = is never reached above has = match } if has || !(len(computeAble) > 1) { m := CompareSelector(baseNode, n) has = m } } } } } if s == ":first-child" { has = n.parent.Children[0].Properties.Id == n.Properties.Id } else if s == ":last-child" { has = n.parent.Children[len(n.parent.Children)-1].Properties.Id == n.Properties.Id } if has { return true, isPsuedo } } return has, isPsuedo } func ParseSelector(selector string) SelectorParts { parts := SelectorParts{ Attribute: make(map[string]string), } // Parse character by character i := 0 length := len(selector) // Parse tag name (if present) start := i for i < length && isValidTagChar(selector[i], i == start) { i++ } if start < i { parts.TagName = selector[start:i] } // Parse the rest of the selector for i < length { switch selector[i] { case '#': // ID i++ // Skip # start = i for i < length && isValidIdClassChar(selector[i]) { i++ } if start < i { parts.Id = selector[start:i] } case '.': // Class i++ // Skip . start = i for i < length && isValidIdClassChar(selector[i]) { i++ } if start < i { parts.ClassList = append(parts.ClassList, selector[start:i]) } case '[': // Attribute i++ // Skip [ start = i var attrName string var attrValue string // Parse attribute name for i < length && selector[i] != '=' && selector[i] != ']' { i++ } attrName = strings.TrimSpace(selector[start:i]) // Parse attribute value if present if i < length && selector[i] == '=' { i++ // Skip = // Skip quotes if present if i < length && (selector[i] == '"' || selector[i] == '\'') { quote := selector[i] i++ // Skip opening quote start = i for i < length && selector[i] != quote { i++ } attrValue = selector[start:i] if i < length { i++ // Skip closing quote } } else { start = i for i < length && selector[i] != ']' { i++ } attrValue = strings.TrimSpace(selector[start:i]) } } // Skip to closing bracket for i < length && selector[i] != ']' { i++ } if i < length { i++ // Skip ] } parts.Attribute[attrName] = attrValue default: i++ // Skip any other character } } return parts } // Helper functions to check character validity func isValidTagChar(c byte, isFirst bool) bool { if isFirst { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') } return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' } func isValidIdClassChar(c byte) bool { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-' } func CompareSelector(selector string, n *Node) bool { baseParts := ParseSelector(selector) has := false if selector == "*" { return true } if baseParts.Id == n.id || baseParts.Id == "" { if baseParts.TagName == n.tagName || baseParts.TagName == "" { match := true for _, v := range baseParts.ClassList { bpc := false for _, c := range n.ClassList.Classes() { if v == c { bpc = true } } if !bpc { match = false } } for k, v := range baseParts.Attribute { if n.attribute[k] != v { match = false } } if match { has = true } } } return has } func ExtractBaseElements(selector string) [][]string { var baseElements []string selectors := splitSelector(selector, ',') for _, s := range selectors { directChild := splitSelector(s, '>') adjacentSibling := splitSelector(directChild[len(directChild)-1], '+') generalSiblings := splitSelector(adjacentSibling[len(adjacentSibling)-1], '~') descendants := splitSelector(generalSiblings[len(generalSiblings)-1], ' ') for _, d := range descendants { computeAble := splitSelector(d, ':') // Add valid base elements to the list if len(computeAble) > 0 && computeAble[0] != "" { baseElements = append(baseElements, computeAble[0]) } } } baseParts := [][]string{} computeAble := splitSelector(selector, ':') for _, v := range computeAble { if strings.Contains(v, "(") { be := ExtractBaseElements(v[strings.Index(v, "(")+1 : len(v)-2]) for _, b := range be { baseElements = append(baseElements, b...) } } } for _, v := range baseElements { ps := ParseSelector(v) selectors := []string{} selectors = append(selectors, ps.TagName) if ps.Id != "" { selectors = append(selectors, "#"+ps.Id) } for _, c := range ps.ClassList { selectors = append(selectors, "."+c) } for k, v := range ps.Attribute { selectors = append(selectors, `[`+k+`="`+v+`"]`) } baseParts = append(baseParts, selectors) } baseParts = append(baseParts, []string{"*"}) return baseParts } func ShouldTestSelector(n *Node, selector string) bool { baseElements := ExtractBaseElements(selector) for _, base := range baseElements { if CompareSelector(strings.Join(base, ""), n) { return true } } return false } func GenBaseElements(n *Node) []string { selectors := []string{} selectors = append(selectors, n.tagName) if n.id != "" { selectors = append(selectors, "#"+n.id) } for _, c := range n.ClassList.Classes() { selectors = append(selectors, "."+c) } for k, v := range n.attribute { selectors = append(selectors, `[`+k+`="`+v+`"]`) } return selectors }