about summary refs log blame commit diff stats
path: root/src/css/match.nim
blob: 02c921b908fe0371e0cdbe5de99b4f46b7031366 (plain) (tree)
1
2
3
4
5
6
7
8
9
                  

                   
 
                 
                    

                         
                 
               
                   
 

                                                              
                

                                          
                     



                                                                  

                                 

                                                             


                                                 

                                                          
                                 
                     


                                                                

                                                 


                                                                

                                 



                                                      

                                 



                                                    

                                 

                                             


                                           
                                          
 

                                                                            
 

                                                                         

                                         


                 

                                                                         


                                 
                   





                                                                   
                                                                
                     



                                                           





                                                       
                                  
                                    







                                                    

                                                         

                


                                                           





                                                       
                                  
                                        

                                           
                        




                                                    

                                                         
             
                
               
                                                                  




                                             
             
                                                                
                                      
           
                                                            
                   
                                                        
            
                                                
            
                                                                    
               
                
 

                                                                   


                                  
            
            
                                    
             
                                      
          
                            
            
                                        
                   
                                                   
                     
               
                 
               
 

                                                                           




                                             

                                                           






                                    
              
                                           
                    





                                         
               


                                             
                     









                                                   

                                               
                           















                                                   


                                                                               

                                                                            
                              


         
                                                  
 
                                                            
                                                          
                               

                                         
                                                                               
 
                                                    
                                                          
                               


                                         
                                                                      
import std/options
import std/strutils
import std/tables

import chame/tags
import css/cssparser
import css/selectorparser
import css/stylednode
import html/catom
import html/dom
import utils/twtstr

#TODO rfNone should match insensitively for certain properties
func attrSelectorMatches(elem: Element; sel: Selector): bool =
  case sel.rel.t
  of rtExists: return elem.attrb(sel.attr)
  of rtEquals:
    case sel.rel.flag
    of rfNone: return elem.attr(sel.attr) == sel.value
    of rfI: return elem.attr(sel.attr).equalsIgnoreCase(sel.value)
    of rfS: return elem.attr(sel.attr) == sel.value
  of rtToken:
    let val = elem.attr(sel.attr)
    case sel.rel.flag
    of rfNone: return sel.value in val.split(AsciiWhitespace)
    of rfI:
      let val = val.toLowerAscii()
      let selval = sel.value.toLowerAscii()
      return selval in val.split(AsciiWhitespace)
    of rfS: return sel.value in val.split(AsciiWhitespace)
  of rtBeginDash:
    let val = elem.attr(sel.attr)
    case sel.rel.flag
    of rfNone:
      return val == sel.value or sel.value.startsWith(val & '-')
    of rfI:
      return val.equalsIgnoreCase(sel.value) or
        sel.value.startsWithIgnoreCase(val & '-')
    of rfS:
      return val == sel.value or sel.value.startsWith(val & '-')
  of rtStartsWith:
    let val = elem.attr(sel.attr)
    case sel.rel.flag
    of rfNone: return val.startsWith(sel.value)
    of rfI: return val.startsWithIgnoreCase(sel.value)
    of rfS: return val.startsWith(sel.value)
  of rtEndsWith:
    let val = elem.attr(sel.attr)
    case sel.rel.flag
    of rfNone: return val.endsWith(sel.value)
    of rfI: return val.endsWithIgnoreCase(sel.value)
    of rfS: return val.endsWith(sel.value)
  of rtContains:
    let val = elem.attr(sel.attr)
    case sel.rel.flag
    of rfNone: return val.contains(sel.value)
    of rfI:
      let val = val.toLowerAscii()
      let selval = sel.value.toLowerAscii()
      return val.contains(selval)
    of rfS: return val.contains(sel.value)

func selectorsMatch*[T: Element|StyledNode](elem: T; cxsel: ComplexSelector;
  felem: T = nil): bool

func selectorsMatch*[T: Element|StyledNode](elem: T; slist: SelectorList;
    felem: T = nil): bool =
  for cxsel in slist:
    if elem.selectorsMatch(cxsel, felem):
      return true
  return false

func pseudoSelectorMatches[T: Element|StyledNode](elem: T; sel: Selector;
    felem: T): bool =
  let selem = elem
  when elem is StyledNode:
    let elem = Element(elem.node)
  case sel.pseudo.t
  of pcFirstChild: return elem.parentNode.firstElementChild == elem
  of pcLastChild: return elem.parentNode.lastElementChild == elem
  of pcOnlyChild:
    return elem.parentNode.firstElementChild == elem and
      elem.parentNode.lastElementChild == elem
  of pcHover:
    when selem is StyledNode: felem.addDependency(elem, dtHover)
    return elem.hover
  of pcRoot: return elem == elem.document.html
  of pcNthChild:
    if sel.pseudo.ofsels.len != 0 and
        not selem.selectorsMatch(sel.pseudo.ofsels, felem):
      return false
    let A = sel.pseudo.anb.A # step
    let B = sel.pseudo.anb.B # start
    var i = 1
    let parent = when selem is StyledNode: selem.parent
    else: selem.parentNode
    if parent == nil: return false
    for child in parent.elementList:
      when selem is StyledNode:
        if not child.isDomElement: continue
      if child == selem:
        if A == 0:
          return i == B
        if A < 0:
          return (i - B) <= 0 and (i - B) mod A == 0
        return (i - B) >= 0 and (i - B) mod A == 0
      if sel.pseudo.ofsels.len == 0 or
          child.selectorsMatch(sel.pseudo.ofsels, felem):
        inc i
    return false
  of pcNthLastChild:
    if sel.pseudo.ofsels.len == 0 and
        not selem.selectorsMatch(sel.pseudo.ofsels, felem):
      return false
    let A = sel.pseudo.anb.A # step
    let B = sel.pseudo.anb.B # start
    var i = 1
    let parent = when selem is StyledNode: selem.parent
    else: selem.parentNode
    if parent == nil: return false
    for child in parent.elementList_rev:
      when selem is StyledNode:
        if not child.isDomElement: continue
      if child == selem:
        if A == 0:
          return i == B
        if A < 0:
          return (i - B) <= 0 and (i - B) mod A == 0
        return (i - B) >= 0 and (i - B) mod A == 0
      if sel.pseudo.ofsels.len != 0 or
          child.selectorsMatch(sel.pseudo.ofsels, felem):
        inc i
    return false
  of pcChecked:
    when selem is StyledNode: felem.addDependency(elem, dtChecked)
    if elem.tagType == TAG_INPUT:
      return HTMLInputElement(elem).checked
    elif elem.tagType == TAG_OPTION:
      return HTMLOptionElement(elem).selected
    return false
  of pcFocus:
    when selem is StyledNode: felem.addDependency(elem, dtFocus)
    return elem.document.focus == elem
  of pcNot:
    return not selem.selectorsMatch(sel.pseudo.fsels, felem)
  of pcIs, pcWhere:
    return selem.selectorsMatch(sel.pseudo.fsels, felem)
  of pcLang:
    return sel.pseudo.s == "en" #TODO languages?
  of pcLink:
    return elem.tagType in {TAG_A, TAG_AREA} and elem.attrb(satHref)
  of pcVisited:
    return false

func selectorMatches[T: Element|StyledNode](elem: T; sel: Selector;
    felem: T = nil): bool =
  let selem = elem
  when elem is StyledNode:
    let elem = Element(selem.node)
  case sel.t
  of stType:
    return elem.localName == sel.tag
  of stClass:
    return sel.class in elem.classList
  of stId:
    return sel.id == elem.id
  of stAttr:
    return elem.attrSelectorMatches(sel)
  of stPseudoClass:
    return pseudoSelectorMatches(selem, sel, felem)
  of stPseudoElement:
    return true
  of stUniversal:
    return true

func selectorsMatch[T: Element|StyledNode](elem: T; sels: CompoundSelector;
    felem: T): bool =
  for sel in sels:
    if not selectorMatches(elem, sel, felem):
      return false
  return true

func complexSelectorMatches[T: Element|StyledNode](elem: T;
    cxsel: ComplexSelector; felem: T = nil): bool =
  var e = elem
  for i in countdown(cxsel.high, 0):
    let sels = cxsel[i]
    if e == nil:
      return false
    var match = false
    case sels.ct
    of ctNone:
      match = e.selectorsMatch(sels, felem)
    of ctDescendant:
      e = e.parentElement
      while e != nil:
        if e.selectorsMatch(sels, felem):
          match = true
          break
        e = e.parentElement
    of ctChild:
      e = e.parentElement
      if e != nil:
        match = e.selectorsMatch(sels, felem)
    of ctNextSibling:
      if e.parentElement == nil: return false
      var found = false
      for child in e.parentElement.elementList_rev:
        when elem is StyledNode:
          if not child.isDomElement: continue
        if e == child:
          found = true
          continue
        if found:
          e = child
          match = e.selectorsMatch(sels, felem)
          break
    of ctSubsequentSibling:
      var found = false
      if e.parentElement == nil: return false
      for child in e.parentElement.elementList_rev:
        when elem is StyledNode:
          if not child.isDomElement: continue
        if child == elem:
          found = true
          continue
        if not found: continue
        if child.selectorsMatch(sels, felem):
          e = child
          match = true
          break
    if not match:
      return false
  return true

# WARNING for StyledNode, this has the side effect of modifying depends.
#TODO make that an explicit flag or something, also get rid of the Element case
func selectorsMatch*[T: Element|StyledNode](elem: T; cxsel: ComplexSelector;
    felem: T = nil): bool =
  var felem = if felem != nil:
    felem
  else:
    elem
  return elem.complexSelectorMatches(cxsel, felem)

proc querySelectorAll(node: Node; q: string): seq[Element] =
  let selectors = parseSelectors(q, node.document.factory)
  for element in node.elements:
    if element.selectorsMatch(selectors):
      result.add(element)
doqsa = (proc(node: Node, q: string): seq[Element] = querySelectorAll(node, q))

proc querySelector(node: Node; q: string): Element =
  let selectors = parseSelectors(q, node.document.factory)
  for element in node.elements:
    if element.selectorsMatch(selectors):
      return element
  return nil
doqs = (proc(node: Node, q: string): Element = querySelector(node, q))