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

                      
                                                                              
 
                    
                              
              
                                                                               
 
                     


                                                                             
 
                        
                                                                     
 
                         
                                   


                                 
                         
 
                                                  

                                             
 
                                                  
                    




                            
                                          
                         
              


                          
            


                          
               


                          
              


                          
                    
                            
                                     
             
                     
                         
                       
                       
 

                        
                                  
                  
                           
                            
                          
              
                

                 




                                                                          

                                      
 



                                                              
                                                                 

                     
                                                                            




                                                   
                                                                 

                    
               
                                 
                                                            
 
                                         
 
                                  
            
            


                        
                                   
          



                                   
            
                            






                         
                                


                 




                                                     
             



                                   
                 
              
                   

                                       
                            



                                        
                        
                   
                                  
                                                                 
                                    
                        
                                      
                         
                                           


                         
                     
                                  


                                           
                  
 



                                           




                                           









                                                 
 

                                         
          
                     
                     
                  
                   
                     
                   
                  
                                    



                                     
                                  
                                    
                    
                                       




                                       
                       
                        
                             
               
                 

           
                                                   
                  

                                 




                                                   
                                        
                             
               
 



                                                            
                                                  

                                                            
                                                                










                                                     
                                                                             
              

                                                             

                                                                 
                     
                     

                                 
                                                           


                                    

                                                                       



                                            

                                                                            


                                


                                                                               
                                     

                                                   




                                                    
                                                        






                                                         

                                                                                
 

                                                                                

                                             
                                                                  
          
                                                                 
             
                                                                    
                 
                                                               
                      
                                                                   

                                






                                                               
                                                        

                            
                
                                                     
                                                                       
                                   










                                                     
                
                

                                       
                                        
                                   

                                              







                                                                     
                                                      
                                         
                                                  



                                                    
                                     
                         
                      
                    
                
                                             
                                        

                                      
                                      
                             





                      
            
                     
                                        
                                                               


                                      
                                                      
                         
                   

                                        
                                        
                                         
                
                                           
                
                  
              
                                           
                       



                          
   



                                                              
                                    
                                             
                                             

                             

                                                                         
                             

                           
                        

                              
                  
                    

                                         
                                               


                            
                  

                                               
                 
                    
                                                
                                             

                                    

                        

                       

                                                
               
                      
                                              
                               
                  
                       
                                  
                                                         
                      
                                       
                                           
                                           


                        
                                

                                                                    
         










                                                                       
                      
                
                     


                                                 
                


                                  

                    
                                                

        
                                                                             

                                                            
                             



                                         
 
                                                                           

                                          
 
                                                          
                          
                                                            
import std/options
import std/strutils

import css/cssparser
import html/catom
import utils/twtstr

type
  SelectorType* = enum
    stType, stId, stAttr, stClass, stUniversal, stPseudoClass, stPseudoElement

  PseudoElem* = enum
    peNone, peBefore, peAfter,
    # internal
    peInputText, peTextareaText, peImage, peNewline, peVideo, peAudio, peCanvas

  PseudoClass* = enum
    pcFirstChild, pcLastChild, pcOnlyChild, pcHover, pcRoot, pcNthChild,
    pcNthLastChild, pcChecked, pcFocus, pcIs, pcNot, pcWhere, pcLang, pcLink,
    pcVisited

  CombinatorType* = enum
    ctNone, ctDescendant, ctChild, ctNextSibling, ctSubsequentSibling

  SelectorParser = object
    selectors: seq[ComplexSelector]
    cvals: seq[CSSComponentValue]
    at: int
    failed: bool
    factory: CAtomFactory

  RelationType* {.size: sizeof(int) div 2.} = enum
    rtExists, rtEquals, rtToken, rtBeginDash,
    rtStartsWith, rtEndsWith, rtContains

  RelationFlag* {.size: sizeof(int) div 2.} = enum
    rfNone, rfI, rfS

  SelectorRelation* = object
    t*: RelationType
    flag*: RelationFlag

  Selector* = ref object # Simple selector
    case t*: SelectorType
    of stType:
      tag*: CAtom
      when defined(debug):
        tags: string
    of stId:
      id*: CAtom
      when defined(debug):
        ids: string
    of stClass:
      class*: CAtom
      when defined(debug):
        classs: string
    of stAttr:
      attr*: CAtom
      when defined(debug):
        attrs: string
      value*: string
      rel*: SelectorRelation
    of stUniversal: #TODO namespaces?
      discard
    of stPseudoClass:
      pseudo*: PseudoData
    of stPseudoElement:
      elem*: PseudoElem

  PseudoData* = object
    case t*: PseudoClass
    of pcNthChild, pcNthLastChild:
      anb*: CSSAnB
      ofsels*: SelectorList
    of pcIs, pcWhere, pcNot:
      fsels*: SelectorList
    of pcLang:
      s*: string
    else: discard

  CompoundSelector* = object
    ct*: CombinatorType # relation to the next entry in a ComplexSelector.
    sels*: seq[Selector]

  ComplexSelector* = seq[CompoundSelector]

  SelectorList* = seq[ComplexSelector]

iterator items*(sels: CompoundSelector): Selector {.inline.} =
  for it in sels.sels:
    yield it

func `[]`*(sels: CompoundSelector; i: int): Selector {.inline.} =
  return sels.sels[i]

func `[]`*(sels: CompoundSelector; i: BackwardsIndex): Selector {.inline.} =
  return sels.sels[i]

func len*(sels: CompoundSelector): int {.inline.} =
  return sels.sels.len

proc add*(sels: var CompoundSelector; sel: Selector) {.inline.} =
  sels.sels.add(sel)

# For debugging
func tostr(ftype: enum): string =
  return ($ftype).split('_')[1..^1].join('-').toLowerAscii()

func `$`*(cxsel: ComplexSelector): string

func `$`*(sel: Selector): string =
  case sel.t
  of stType:
    when defined(debug):
      return sel.tags
    else:
      return "ATOM" & $int(sel.tag)
  of stId:
    when defined(debug):
      return "#" & sel.ids
    else:
      return "#ATOM" & $int(sel.id)
  of stAttr:
    let rel = case sel.rel.t
    of rtExists: ""
    of rtEquals: "="
    of rtToken: "~="
    of rtBeginDash: "|="
    of rtStartsWith: "^="
    of rtEndsWith: "$="
    of rtContains: "*="
    let flag = case sel.rel.flag
    of rfNone: ""
    of rfI: " i"
    of rfS: " s"
    let attrs = when defined(debug):
      sel.attrs
    else:
      "ATOM" & $int(sel.attr)
    return '[' & attrs & rel & sel.value & flag & ']'
  of stClass:
    when defined(debug):
      return "." & sel.classs
    else:
      return ".ATOM" & $int(sel.id)
  of stUniversal:
    return "*"
  of stPseudoClass:
    result = ':' & sel.pseudo.t.tostr()
    case sel.pseudo.t
    of pcIs, pcNot, pcWhere:
      result &= '('
      for fsel in sel.pseudo.fsels:
        result &= $fsel
        if fsel != sel.pseudo.fsels[^1]:
          result &= ", "
      result &= ')'
    of pcNthChild, pcNthLastChild:
      result &= '(' & $sel.pseudo.anb.A & 'n' & $sel.pseudo.anb.B
      if sel.pseudo.ofsels.len != 0:
        result &= " of "
        for fsel in sel.pseudo.ofsels:
          result &= $fsel
          if fsel != sel.pseudo.ofsels[^1]:
            result &= ','
      result &= ')'
    else: discard
  of stPseudoElement:
    return "::" & sel.elem.tostr()

func `$`*(sels: CompoundSelector): string =
  for sel in sels:
    result &= $sel

func `$`*(cxsel: ComplexSelector): string =
  for sels in cxsel:
    result &= $sels
    case sels.ct
    of ctDescendant: result &= ' '
    of ctChild: result &= " > "
    of ctNextSibling: result &= " + "
    of ctSubsequentSibling: result &= " ~ "
    of ctNone: discard

func `$`*(slist: SelectorList): string =
  var s = false
  for cxsel in slist:
    if s:
      result &= ", "
    result &= $cxsel
    s = true

func getSpecificity*(cxsel: ComplexSelector): int

func getSpecificity(sel: Selector): int =
  case sel.t
  of stId:
    result += 1000000
  of stClass, stAttr:
    result += 1000
  of stPseudoClass:
    case sel.pseudo.t
    of pcIs, pcNot:
      var best = 0
      for child in sel.pseudo.fsels:
        let s = getSpecificity(child)
        if s > best:
          best = s
      result += best
    of pcNthChild, pcNthLastChild:
      if sel.pseudo.ofsels.len != 0:
        var best = 0
        for child in sel.pseudo.ofsels:
          let s = getSpecificity(child)
          if s > best:
            best = s
        result += best
      result += 1000
    of pcWhere: discard
    else: result += 1000
  of stType, stPseudoElement:
    result += 1
  of stUniversal:
    discard

func getSpecificity*(sels: CompoundSelector): int =
  for sel in sels:
    result += getSpecificity(sel)

func getSpecificity*(cxsel: ComplexSelector): int =
  for sels in cxsel:
    result += getSpecificity(sels)

func pseudo*(cxsel: ComplexSelector): PseudoElem =
  if cxsel[^1][^1].t == stPseudoElement:
    return cxsel[^1][^1].elem
  return peNone

proc consume(state: var SelectorParser): CSSComponentValue =
  result = state.cvals[state.at]
  inc state.at

proc has(state: var SelectorParser; i = 0): bool =
  return not state.failed and state.at + i < state.cvals.len

proc peek(state: var SelectorParser; i = 0): CSSComponentValue =
  return state.cvals[state.at + i]

template fail() =
  state.failed = true
  return

template get_tok(cval: CSSComponentValue): CSSToken =
  let c = cval
  if not (c of CSSToken): fail
  CSSToken(c)

proc parseSelectorList(cvals: seq[CSSComponentValue]; factory: CAtomFactory):
  SelectorList

# Functions that may contain other selectors, functions, etc.
proc parseRecursiveSelectorFunction(state: var SelectorParser;
    class: PseudoClass; body: seq[CSSComponentValue]): Selector =
  var fun = Selector(
    t: stPseudoClass,
    pseudo: PseudoData(t: class),
  )
  fun.pseudo.fsels = parseSelectorList(body, state.factory)
  if fun.pseudo.fsels.len == 0: fail
  return fun

proc parseNthChild(state: var SelectorParser; cssfunction: CSSFunction;
    data: PseudoData): Selector =
  var data = data
  var (anb, i) = parseAnB(cssfunction.value)
  if anb.isNone: fail
  data.anb = anb.get
  var nthchild = Selector(t: stPseudoClass, pseudo: data)
  while i < cssfunction.value.len and cssfunction.value[i] == cttWhitespace:
    inc i
  if i >= cssfunction.value.len:
    return nthchild
  let lasttok = get_tok cssfunction.value[i]
  if lasttok.tokenType != cttIdent or not lasttok.value.equalsIgnoreCase("of"):
    fail
  if i == cssfunction.value.len: fail
  nthchild.pseudo.ofsels = cssfunction.value[i..^1]
    .parseSelectorList(state.factory)
  if nthchild.pseudo.ofsels.len == 0: fail
  return nthchild

proc skipWhitespace(state: var SelectorParser) =
  while state.has() and state.peek() of CSSToken and
      CSSToken(state.peek()).tokenType == cttWhitespace:
    inc state.at

proc parseLang(cvals: seq[CSSComponentValue]): Selector =
  var state = SelectorParser(cvals: cvals)
  state.skipWhitespace()
  if not state.has(): fail
  let tok = get_tok state.consume()
  if tok.tokenType != cttIdent: fail
  return Selector(t: stPseudoClass, pseudo: PseudoData(t: pcLang, s: tok.value))

proc parseSelectorFunction(state: var SelectorParser; cssfunction: CSSFunction):
    Selector =
  return case cssfunction.name.toLowerAscii()
  of "not":
    state.parseRecursiveSelectorFunction(pcNot, cssfunction.value)
  of "is":
    state.parseRecursiveSelectorFunction(pcIs, cssfunction.value)
  of "where":
    state.parseRecursiveSelectorFunction(pcWhere, cssfunction.value)
  of "nth-child":
    state.parseNthChild(cssfunction, PseudoData(t: pcNthChild))
  of "nth-last-child":
    state.parseNthChild(cssfunction, PseudoData(t: pcNthLastChild))
  of "lang":
    parseLang(cssfunction.value)
  else: fail

proc parsePseudoSelector(state: var SelectorParser): Selector =
  if not state.has(): fail
  let cval = state.consume()
  if cval of CSSToken:
    template add_pseudo_element(element: PseudoElem) =
      return Selector(t: stPseudoElement, elem: element)
    let tok = CSSToken(cval)
    case tok.tokenType
    of cttIdent:
      template add_pseudo_class(class: PseudoClass) =
        return Selector(t: stPseudoClass, pseudo: PseudoData(t: class))
      case tok.value.toLowerAscii()
      of "before": add_pseudo_element peBefore
      of "after": add_pseudo_element peAfter
      of "first-child": add_pseudo_class pcFirstChild
      of "last-child": add_pseudo_class pcLastChild
      of "only-child": add_pseudo_class pcOnlyChild
      of "hover": add_pseudo_class pcHover
      of "root": add_pseudo_class pcRoot
      of "checked": add_pseudo_class pcChecked
      of "focus": add_pseudo_class pcFocus
      of "link": add_pseudo_class pcLink
      of "visited": add_pseudo_class pcVisited
      else: fail
    of cttColon:
      if not state.has(): fail
      let tok = get_tok state.consume()
      if tok.tokenType != cttIdent: fail
      case tok.value.toLowerAscii()
      of "before": add_pseudo_element peBefore
      of "after": add_pseudo_element peAfter
      else: fail
    else: fail
  elif cval of CSSFunction:
    return state.parseSelectorFunction(CSSFunction(cval))
  else: fail

proc parseComplexSelector(state: var SelectorParser): ComplexSelector

proc parseAttributeSelector(state: var SelectorParser;
    cssblock: CSSSimpleBlock): Selector =
  if cssblock.token.tokenType != cttLbracket: fail
  var state2 = SelectorParser(cvals: cssblock.value)
  state2.skipWhitespace()
  if not state2.has(): fail
  let attr = get_tok state2.consume()
  if attr.tokenType != cttIdent: fail
  state2.skipWhitespace()
  if not state2.has():
    return Selector(
      t: stAttr,
      attr: state.factory.toAtom(attr.value),
      rel: SelectorRelation(t: rtExists)
    )
  let delim = get_tok state2.consume()
  if delim.tokenType != cttDelim: fail
  let rel = case delim.cvalue
  of '~': rtToken
  of '|': rtBeginDash
  of '^': rtStartsWith
  of '$': rtEndsWith
  of '*': rtContains
  of '=': rtEquals
  else: fail
  if rel != rtEquals:
    let delim = get_tok state2.consume()
    if delim.tokenType != cttDelim or delim.cvalue != '=': fail
  state2.skipWhitespace()
  if not state2.has(): fail
  let value = get_tok state2.consume()
  if value.tokenType notin {cttIdent, cttString}: fail
  state2.skipWhitespace()
  var flag = rfNone
  if state2.has():
    let delim = get_tok state2.consume()
    if delim.tokenType != cttIdent: fail
    if delim.value.equalsIgnoreCase("i"):
      flag = rfI
    elif delim.value.equalsIgnoreCase("s"):
      flag = rfS
  return Selector(
    t: stAttr,
    attr: state.factory.toAtom(attr.value),
    value: value.value,
    rel: SelectorRelation(
      t: rel,
      flag: flag
    )
  )

proc parseClassSelector(state: var SelectorParser): Selector =
  if not state.has(): fail
  let tok = get_tok state.consume()
  if tok.tokenType != cttIdent: fail
  let class = state.factory.toAtom(tok.value)
  result = Selector(t: stClass, class: class)
  when defined(debug):
    result.classs = tok.value

proc parseCompoundSelector(state: var SelectorParser): CompoundSelector =
  result = CompoundSelector()
  while state.has():
    let cval = state.peek()
    if cval of CSSToken:
      let tok = CSSToken(cval)
      case tok.tokenType
      of cttIdent:
        inc state.at
        let s = tok.value.toLowerAscii()
        let tag = state.factory.toAtom(s)
        let sel = Selector(t: stType, tag: tag)
        when defined(debug):
          sel.tags = s
        result.add(sel)
      of cttColon:
        inc state.at
        result.add(state.parsePseudoSelector())
      of cttHash:
        inc state.at
        let id = state.factory.toAtom(tok.value)
        result.add(Selector(t: stId, id: id))
        when defined(debug):
          result[^1].ids = tok.value
      of cttComma: break
      of cttDelim:
        case tok.cvalue
        of '.':
          inc state.at
          result.add(state.parseClassSelector())
        of '*':
          inc state.at
          result.add(Selector(t: stUniversal))
        of '>', '+', '~': break
        else: fail
      of cttWhitespace:
        # skip trailing whitespace
        if not state.has(1) or state.peek(1) == cttComma:
          inc state.at
        elif state.peek(1) == cttDelim:
          let tok = CSSToken(state.peek(1))
          if tok.cvalue in {'>', '+', '~'}:
            inc state.at
        break
      else: fail
    elif cval of CSSSimpleBlock:
      inc state.at
      result.add(state.parseAttributeSelector(CSSSimpleBlock(cval)))
    else:
      fail

proc parseComplexSelector(state: var SelectorParser): ComplexSelector =
  while true:
    state.skipWhitespace()
    let sels = state.parseCompoundSelector()
    result.add(sels)
    if sels.len == 0: fail
    if not state.has():
      break # finish
    let tok = get_tok state.consume()
    case tok.tokenType
    of cttDelim:
      case tok.cvalue
      of '>': result[^1].ct = ctChild
      of '+': result[^1].ct = ctNextSibling
      of '~': result[^1].ct = ctSubsequentSibling
      else: fail
    of cttWhitespace:
      result[^1].ct = ctDescendant
    of cttComma:
      break # finish
    else: fail
  if result.len == 0 or result[^1].ct != ctNone:
    fail

proc parseSelectorList(cvals: seq[CSSComponentValue]; factory: CAtomFactory):
    SelectorList =
  var state = SelectorParser(cvals: cvals, factory: factory)
  var res: SelectorList = @[]
  while state.has():
    res.add(state.parseComplexSelector())
  if not state.failed:
    return res

proc parseSelectors*(cvals: seq[CSSComponentValue]; factory: CAtomFactory):
    seq[ComplexSelector] =
  return parseSelectorList(cvals, factory)

proc parseSelectors*(ibuf: string; factory: CAtomFactory):
    seq[ComplexSelector] =
  return parseSelectors(parseComponentValues(ibuf), factory)