about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--src/css/match.nim155
-rw-r--r--src/css/selectorparser.nim620
-rw-r--r--src/css/sheet.nim37
-rw-r--r--src/css/stylednode.nim4
4 files changed, 365 insertions, 451 deletions
diff --git a/src/css/match.nim b/src/css/match.nim
index b17c690d..b05fd392 100644
--- a/src/css/match.nim
+++ b/src/css/match.nim
@@ -22,11 +22,11 @@ func attrSelectorMatches(elem: Element, sel: Selector): bool =
   of '*': return elem.attr(sel.attr).contains(sel.value)
   else: return false
 
-func selectorsMatch*[T: Element|StyledNode](elem: T, selectors: ComplexSelector, felem: T = nil): bool
+func selectorsMatch*[T: Element|StyledNode](elem: T, cxsel: ComplexSelector, felem: T = nil): bool
 
-func selectorsMatch*[T: Element|StyledNode](elem: T, selectors: SelectorList, felem: T = nil): bool =
-  for slist in selectors:
-    if elem.selectorsMatch(slist, felem):
+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
 
@@ -43,7 +43,7 @@ func pseudoSelectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem:
     return elem.hover
   of PSEUDO_ROOT: return elem == elem.document.html
   of PSEUDO_NTH_CHILD:
-    if sel.pseudo.ofsels.issome and not selem.selectorsMatch(sel.pseudo.ofsels.get, felem):
+    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
@@ -60,11 +60,11 @@ func pseudoSelectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem:
         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.isnone or child.selectorsMatch(sel.pseudo.ofsels.get, felem):
+      if sel.pseudo.ofsels.len == 0 or child.selectorsMatch(sel.pseudo.ofsels, felem):
         inc i
     return false
   of PSEUDO_NTH_LAST_CHILD:
-    if sel.pseudo.ofsels.issome and not selem.selectorsMatch(sel.pseudo.ofsels.get, felem):
+    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
@@ -81,7 +81,7 @@ func pseudoSelectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem:
         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.isnone or child.selectorsMatch(sel.pseudo.ofsels.get, felem):
+      if sel.pseudo.ofsels.len != 0 or child.selectorsMatch(sel.pseudo.ofsels, felem):
         inc i
     return false
   of PSEUDO_CHECKED:
@@ -105,75 +105,6 @@ func pseudoSelectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem:
   of PSEUDO_VISITED:
     return false
 
-func combinatorSelectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem: T): bool =
-  let selem = elem
-  # combinator without at least two members makes no sense
-  # actually, combinators with more than two elements are a pretty bad idea
-  # too. TODO getting rid of them would simplify this function greatly
-  assert sel.csels.len > 1
-  if selem.selectorsMatch(sel.csels[^1], felem):
-    var i = sel.csels.len - 2
-    case sel.ct
-    of DESCENDANT_COMBINATOR:
-      when selem is StyledNode:
-        var e = elem.parent
-      else:
-        var e = elem.parentElement
-      while e != nil and i >= 0:
-        if e.selectorsMatch(sel.csels[i], felem):
-          dec i
-        when elem is StyledNode:
-          e = e.parent
-        else:
-          e = e.parentElement
-    of CHILD_COMBINATOR:
-      when elem is StyledNode:
-        var e = elem.parent
-      else:
-        var e = elem.parentElement
-      while e != nil and i >= 0:
-        if not e.selectorsMatch(sel.csels[i], felem):
-          return false
-        dec i
-        when elem is StyledNode:
-          e = e.parent
-        else:
-          e = e.parentElement
-    of NEXT_SIBLING_COMBINATOR:
-      var found = false
-      let parent = when elem is StyledNode: elem.parent
-      else: elem.parentElement
-      if parent == nil: return false
-      for child in parent.elementList_rev:
-        when elem is StyledNode:
-          if not child.isDomElement: continue
-        if found:
-          if not child.selectorsMatch(sel.csels[i], felem):
-            return false
-          dec i
-          if i < 0:
-            return true
-        if child == elem:
-          found = true
-    of SUBSEQ_SIBLING_COMBINATOR:
-      var found = false
-      let parent = when selem is StyledNode: selem.parent
-      else: elem.parentElement
-      if parent == nil: return false
-      for child in parent.elementList_rev:
-        when selem is StyledNode:
-          if not child.isDomElement: continue
-        if child == selem:
-          found = true
-          continue
-        if not found: continue
-        if child.selectorsMatch(sel.csels[i], felem):
-          dec i
-        if i < 0:
-          return true
-    return i == -1
-  return false
-
 func selectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem: T = nil): bool =
   let selem = elem
   when elem is StyledNode:
@@ -193,21 +124,73 @@ func selectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem: T = n
     return true
   of UNIVERSAL_SELECTOR:
     return true
-  of COMBINATOR_SELECTOR:
-    return combinatorSelectorMatches(selem, sel, felem)
+
+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 NO_COMBINATOR:
+      match = e.selectorsMatch(sels, felem):
+    of DESCENDANT_COMBINATOR:
+      e = e.parentElement
+      while e != nil:
+        if e.selectorsMatch(sels, felem):
+          match = true
+          break
+        e = e.parentElement
+    of CHILD_COMBINATOR:
+      e = e.parentElement
+      if e != nil:
+        match = e.selectorsMatch(sels, felem)
+    of NEXT_SIBLING_COMBINATOR:
+      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
+          if not e.selectorsMatch(sels, felem):
+            return false
+    of SUBSEQ_SIBLING_COMBINATOR:
+      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, selectors: ComplexSelector, felem: T = nil): bool =
-  let felem = if felem != nil:
+func selectorsMatch*[T: Element|StyledNode](elem: T, cxsel: ComplexSelector, felem: T = nil): bool =
+  var felem = if felem != nil:
     felem
   else:
     elem
-
-  for sel in selectors:
-    if not selectorMatches(elem, sel, felem):
-      return false
-  return true
+  return elem.complexSelectorMatches(cxsel, felem)
 
 proc querySelectorAll(node: Node, q: string): seq[Element] =
   let selectors = parseSelectors(newStringStream(q))
diff --git a/src/css/selectorparser.nim b/src/css/selectorparser.nim
index b10c8f45..dad58671 100644
--- a/src/css/selectorparser.nim
+++ b/src/css/selectorparser.nim
@@ -9,13 +9,7 @@ import html/tags
 type
   SelectorType* = enum
     TYPE_SELECTOR, ID_SELECTOR, ATTR_SELECTOR, CLASS_SELECTOR,
-    UNIVERSAL_SELECTOR, PSEUDO_SELECTOR, PSELEM_SELECTOR, COMBINATOR_SELECTOR
-
-  QueryMode = enum
-    QUERY_TYPE, QUERY_CLASS, QUERY_ATTR, QUERY_DELIM, QUERY_VALUE,
-    QUERY_PSEUDO, QUERY_PSELEM, QUERY_DESC_COMBINATOR, QUERY_CHILD_COMBINATOR,
-    QUERY_NEXT_SIBLING_COMBINATOR, QUERY_SUBSEQ_SIBLING_COMBINATOR,
-    QUERY_FAILED
+    UNIVERSAL_SELECTOR, PSEUDO_SELECTOR, PSELEM_SELECTOR
 
   PseudoElem* = enum
     PSEUDO_NONE, PSEUDO_BEFORE, PSEUDO_AFTER,
@@ -29,15 +23,16 @@ type
     PSEUDO_LINK, PSEUDO_VISITED
 
   CombinatorType* = enum
-    DESCENDANT_COMBINATOR, CHILD_COMBINATOR, NEXT_SIBLING_COMBINATOR,
-    SUBSEQ_SIBLING_COMBINATOR
+    NO_COMBINATOR, DESCENDANT_COMBINATOR, CHILD_COMBINATOR,
+    NEXT_SIBLING_COMBINATOR, SUBSEQ_SIBLING_COMBINATOR
 
   SelectorParser = object
     selectors: seq[ComplexSelector]
-    query: QueryMode
-    combinator: Selector
+    cvals: seq[CSSComponentValue]
+    at: int
+    failed: bool
 
-  Selector* = ref object # compound selector
+  Selector* = ref object # Simple selector
     case t*: SelectorType
     of TYPE_SELECTOR:
       tag*: TagType
@@ -55,32 +50,47 @@ type
       pseudo*: PseudoData
     of PSELEM_SELECTOR:
       elem*: PseudoElem
-    of COMBINATOR_SELECTOR:
-      ct*: CombinatorType
-      csels*: SelectorList
 
   PseudoData* = object
     case t*: PseudoClass
     of PSEUDO_NTH_CHILD, PSEUDO_NTH_LAST_CHILD:
       anb*: CSSAnB
-      ofsels*: Option[SelectorList]
+      ofsels*: SelectorList
     of PSEUDO_IS, PSEUDO_WHERE, PSEUDO_NOT:
       fsels*: SelectorList
     of PSEUDO_LANG:
       s*: string
     else: discard
 
-  # Kind of an oversimplification, but the distinction between complex and
-  # compound selectors isn't too significant.
-  ComplexSelector* = seq[Selector]
+  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("-").tolower()
 
-func `$`*(sellist: ComplexSelector): string
+func `$`*(cxsel: ComplexSelector): string
 
 func `$`*(sel: Selector): string =
   case sel.t
@@ -109,37 +119,44 @@ func `$`*(sel: Selector): string =
       for fsel in sel.pseudo.fsels:
         result &= $fsel
         if fsel != sel.pseudo.fsels[^1]:
-          result &= ','
+          result &= ", "
       result &= ')'
     of PSEUDO_NTH_CHILD, PSEUDO_NTH_LAST_CHILD:
       result &= '(' & $sel.pseudo.anb.A & 'n' & $sel.pseudo.anb.B
-      if sel.pseudo.ofsels.isSome:
+      if sel.pseudo.ofsels.len != 0:
         result &= " of "
-        for fsel in sel.pseudo.ofsels.get:
+        for fsel in sel.pseudo.ofsels:
           result &= $fsel
-          if fsel != sel.pseudo.ofsels.get[^1]:
+          if fsel != sel.pseudo.ofsels[^1]:
             result &= ','
       result &= ')'
     else: discard
   of PSELEM_SELECTOR:
     return "::" & sel.elem.tostr()
-  of COMBINATOR_SELECTOR:
-    var delim: char
-    case sel.ct
-    of DESCENDANT_COMBINATOR: delim = ' '
-    of CHILD_COMBINATOR: delim = '>'
-    of NEXT_SIBLING_COMBINATOR: delim = '+'
-    of SUBSEQ_SIBLING_COMBINATOR: delim = '~'
-    for i in 0 ..< sel.csels.len:
-      result &= $sel.csels[i]
-      if i == 0 or i != sel.csels.high:
-        result &= delim
-
-func `$`*(sellist: ComplexSelector): string =
-  for sel in sellist:
+
+func `$`*(sels: CompoundSelector): string =
+  for sel in sels:
     result &= $sel
 
-func getSpecificity*(sels: ComplexSelector): int
+func `$`*(cxsel: ComplexSelector): string =
+  for sels in cxsel:
+    result &= $sels
+    case sels.ct
+    of DESCENDANT_COMBINATOR: result &= ' '
+    of CHILD_COMBINATOR: result &= " > "
+    of NEXT_SIBLING_COMBINATOR: result &= " + "
+    of SUBSEQ_SIBLING_COMBINATOR: result &= " ~ "
+    of NO_COMBINATOR: 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
@@ -157,9 +174,9 @@ func getSpecificity(sel: Selector): int =
           best = s
       result += best
     of PSEUDO_NTH_CHILD, PSEUDO_NTH_LAST_CHILD:
-      if sel.pseudo.ofsels.isSome:
+      if sel.pseudo.ofsels.len != 0:
         var best = 0
-        for child in sel.pseudo.ofsels.get:
+        for child in sel.pseudo.ofsels:
           let s = getSpecificity(child)
           if s > best:
             best = s
@@ -171,325 +188,236 @@ func getSpecificity(sel: Selector): int =
     result += 1
   of UNIVERSAL_SELECTOR:
     discard
-  of COMBINATOR_SELECTOR:
-    for child in sel.csels:
-      result += getSpecificity(child)
 
-func getSpecificity*(sels: ComplexSelector): int =
+func getSpecificity*(sels: CompoundSelector): int =
   for sel in sels:
     result += getSpecificity(sel)
 
-func pseudo*(sels: ComplexSelector): PseudoElem =
-  var sel = sels[^1]
-  while sel.t == COMBINATOR_SELECTOR:
-    sel = sel.csels[^1][^1]
-  if sel.t == PSELEM_SELECTOR:
-    return sel.elem
+func getSpecificity*(cxsel: ComplexSelector): int =
+  for sels in cxsel:
+    result += getSpecificity(sels)
+
+func pseudo*(cxsel: ComplexSelector): PseudoElem =
+  if cxsel[^1][^1].t == PSELEM_SELECTOR:
+    return cxsel[^1][^1].elem
   return PSEUDO_NONE
 
-proc flushCombinator(state: var SelectorParser) =
-  if state.combinator != nil:
-    if state.combinator.csels.len == 1:
-      if state.combinator.ct == DESCENDANT_COMBINATOR:
-        # otherwise it's an invalid combinator
-        state.selectors[^1].add(state.combinator.csels[0])
-      else:
-        state.query = QUERY_FAILED
-    elif state.combinator.csels[^1].len != 0:
-      state.selectors[^1].add(state.combinator)
-    else:
-      state.query = QUERY_FAILED
-    state.combinator = nil
-
-proc addSelector(state: var SelectorParser, sel: Selector) =
-  if state.combinator != nil:
-    state.combinator.csels[^1].add(sel)
-  else:
-    state.selectors[^1].add(sel)
-
-proc getLastSel(state: SelectorParser): Selector =
-  if state.combinator != nil:
-    return state.combinator.csels[^1][^1]
-  else:
-    return state.selectors[^1][^1]
-
-proc addComplexSelector(state: var SelectorParser) =
-  state.flushCombinator()
-  state.selectors.add(newSeq[Selector]())
-
-func getComplexSelectors(state: var SelectorParser): seq[ComplexSelector] =
-  state.flushCombinator()
-  return state.selectors
-
-proc parseSelectorCombinator(state: var SelectorParser, ct: CombinatorType, csstoken: CSSToken) =
-  if csstoken.tokenType notin {CSS_IDENT_TOKEN, CSS_HASH_TOKEN, CSS_COLON_TOKEN} and
-     (csstoken.tokenType != CSS_DELIM_TOKEN or (csstoken.rvalue != Rune('.') and csstoken.rvalue != Rune('*'))):
-    return
-  if state.combinator != nil and state.combinator.ct != ct:
-    let nc = Selector(t: COMBINATOR_SELECTOR, ct: ct)
-    nc.csels.add(@[state.combinator])
-    state.combinator = nc
-
-  if state.combinator == nil:
-    state.combinator = Selector(t: COMBINATOR_SELECTOR, ct: ct)
-
-  state.combinator.csels.add(state.selectors[^1])
-  if state.combinator.csels[^1].len > 0:
-    state.combinator.csels.add(newSeq[Selector]())
-  state.selectors[^1].setLen(0)
-  state.query = QUERY_TYPE
-
-proc parseSelectorToken(state: var SelectorParser, csstoken: CSSToken) =
-  if csstoken.tokenType == CSS_WHITESPACE_TOKEN:
-    # do not interpret whitespace before/after a combinator selector token
-    if state.query in {QUERY_CHILD_COMBINATOR, QUERY_NEXT_SIBLING_COMBINATOR,
-        QUERY_SUBSEQ_SIBLING_COMBINATOR}:
-      return
-    elif state.combinator != nil and state.combinator.csels[^1].len <= 1:
-      return
-  case state.query
-  of QUERY_DESC_COMBINATOR:
-    state.parseSelectorCombinator(DESCENDANT_COMBINATOR, csstoken)
-  of QUERY_CHILD_COMBINATOR:
-    state.parseSelectorCombinator(CHILD_COMBINATOR, csstoken)
-  of QUERY_NEXT_SIBLING_COMBINATOR:
-    state.parseSelectorCombinator(NEXT_SIBLING_COMBINATOR, csstoken)
-  of QUERY_SUBSEQ_SIBLING_COMBINATOR:
-    state.parseSelectorCombinator(SUBSEQ_SIBLING_COMBINATOR, csstoken)
-  else: discard
-
-  template add_pseudo_element(element: PseudoElem) =
-    state.addSelector(Selector(t: PSELEM_SELECTOR, elem: element))
-  case csstoken.tokenType
-  of CSS_IDENT_TOKEN:
-    case state.query
-    of QUERY_CLASS:
-      state.addSelector(Selector(t: CLASS_SELECTOR, class: csstoken.value))
-    of QUERY_TYPE:
-      state.addSelector(Selector(t: TYPE_SELECTOR, tag: tagType(csstoken.value)))
-    of QUERY_PSEUDO:
+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]): SelectorList
+
+# Functions that may contain other selectors, functions, etc.
+proc parseRecursiveSelectorFunction(state: var SelectorParser, class: PseudoClass, body: seq[CSSComponentValue]): Selector =
+  var fun = Selector(
+    t: PSEUDO_SELECTOR,
+    pseudo: PseudoData(t: class),
+  )
+  fun.pseudo.fsels = parseSelectorList(body)
+  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: PSEUDO_SELECTOR, pseudo: data)
+  while i < cssfunction.value.len and cssfunction.value[i] == CSS_WHITESPACE_TOKEN:
+    inc i
+  if i >= cssfunction.value.len:
+    return nthchild
+  if (get_tok cssfunction.value[i]).value != "of": fail
+  if i == cssfunction.value.len: fail
+  nthchild.pseudo.ofsels = parseSelectorList(cssfunction.value[i..^1])
+  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 == CSS_WHITESPACE_TOKEN:
+    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 != CSS_IDENT_TOKEN: fail
+  return Selector(t: PSEUDO_SELECTOR, pseudo: PseudoData(t: PSEUDO_LANG, s: tok.value))
+
+proc parseSelectorFunction(state: var SelectorParser, cssfunction: CSSFunction): Selector =
+  case cssfunction.name
+  of "not": return state.parseRecursiveSelectorFunction(PSEUDO_NOT, cssfunction.value)
+  of "is": return state.parseRecursiveSelectorFunction(PSEUDO_IS, cssfunction.value)
+  of "where": return state.parseRecursiveSelectorFunction(PSEUDO_WHERE, cssfunction.value)
+  of "nth-child": return state.parseNthChild(cssfunction, PseudoData(t: PSEUDO_NTH_CHILD))
+  of "nth-last-child": return state.parseNthChild(cssfunction, PseudoData(t: PSEUDO_NTH_LAST_CHILD))
+  of "lang": return 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: PSELEM_SELECTOR, elem: element)
+    let tok = CSSToken(cval)
+    case tok.tokenType
+    of CSS_IDENT_TOKEN:
       template add_pseudo_class(class: PseudoClass) =
-        state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: PseudoData(t: class)))
-      case csstoken.value
-      of "before":
-        add_pseudo_element PSEUDO_BEFORE
-      of "after":
-        add_pseudo_element PSEUDO_AFTER
-      of "first-child":
-        add_pseudo_class PSEUDO_FIRST_CHILD
-      of "last-child":
-        add_pseudo_class PSEUDO_LAST_CHILD
-      of "only-child":
-        add_pseudo_class PSEUDO_ONLY_CHILD
-      of "hover":
-        add_pseudo_class PSEUDO_HOVER
-      of "root":
-        add_pseudo_class PSEUDO_ROOT
-      of "checked":
-        add_pseudo_class PSEUDO_CHECKED
-      of "focus":
-        add_pseudo_class PSEUDO_FOCUS
-      of "link":
-        add_pseudo_class PSEUDO_LINK
-      of "visited":
-        add_pseudo_class PSEUDO_VISITED
-      else:
-        state.query = QUERY_FAILED
-        return
-    of QUERY_PSELEM:
-      case csstoken.value
-      of "before":
-        add_pseudo_element PSEUDO_BEFORE
-      of "after":
-        add_pseudo_element PSEUDO_AFTER
-      else:
-        state.query = QUERY_FAILED
-        return
-    else:
-      state.query = QUERY_FAILED
-      return
-    state.query = QUERY_TYPE
-  of CSS_DELIM_TOKEN:
-    case csstoken.rvalue
-    of Rune('.'):
-      state.query = QUERY_CLASS
-    of Rune('>'):
-      if state.selectors[^1].len > 0 or state.combinator != nil:
-        state.query = QUERY_CHILD_COMBINATOR
-    of Rune('+'):
-      if state.selectors[^1].len > 0 or state.combinator != nil:
-        state.query = QUERY_NEXT_SIBLING_COMBINATOR
-    of Rune('~'):
-      if state.selectors[^1].len > 0 or state.combinator != nil:
-        state.query = QUERY_SUBSEQ_SIBLING_COMBINATOR
-    of Rune('*'):
-      state.addSelector(Selector(t: UNIVERSAL_SELECTOR))
-    else:
-      state.query = QUERY_FAILED
-      return
-  of CSS_HASH_TOKEN:
-    state.addSelector(Selector(t: ID_SELECTOR, id: csstoken.value))
-  of CSS_COMMA_TOKEN:
-    state.flushCombinator()
-    if state.selectors[^1].len > 0:
-      state.addComplexSelector()
-  of CSS_WHITESPACE_TOKEN:
-    if state.selectors[^1].len > 0 or state.combinator != nil and state.combinator.csels[^1].len > 0:
-      state.query = QUERY_DESC_COMBINATOR
-  of CSS_COLON_TOKEN:
-    if state.query == QUERY_PSEUDO:
-      state.query = QUERY_PSELEM
-    else:
-      state.query = QUERY_PSEUDO
-  else:
-    state.query = QUERY_FAILED
-    return
-
-proc parseSelectorSimpleBlock(state: var SelectorParser, cssblock: CSSSimpleBlock) =
-  if cssblock.token.tokenType != CSS_LBRACKET_TOKEN:
-    state.query = QUERY_FAILED
-    return
-  state.query = QUERY_ATTR
-  for cval in cssblock.value:
+        return Selector(t: PSEUDO_SELECTOR, pseudo: PseudoData(t: class))
+      case tok.value
+      of "before": add_pseudo_element PSEUDO_BEFORE
+      of "after": add_pseudo_element PSEUDO_AFTER
+      of "first-child": add_pseudo_class PSEUDO_FIRST_CHILD
+      of "last-child": add_pseudo_class PSEUDO_LAST_CHILD
+      of "only-child": add_pseudo_class PSEUDO_ONLY_CHILD
+      of "hover": add_pseudo_class PSEUDO_HOVER
+      of "root": add_pseudo_class PSEUDO_ROOT
+      of "checked": add_pseudo_class PSEUDO_CHECKED
+      of "focus": add_pseudo_class PSEUDO_FOCUS
+      of "link": add_pseudo_class PSEUDO_LINK
+      of "visited": add_pseudo_class PSEUDO_VISITED
+      else: fail
+    of CSS_COLON_TOKEN:
+      if not state.has(): fail
+      let tok = get_tok state.consume()
+      if tok.tokenType != CSS_IDENT_TOKEN: fail
+      case tok.value
+      of "before": add_pseudo_element PSEUDO_BEFORE
+      of "after": add_pseudo_element PSEUDO_AFTER
+      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 != CSS_LBRACKET_TOKEN: fail
+  var state2 = SelectorParser(cvals: cssblock.value)
+  state2.skipWhitespace()
+  if not state2.has(): fail
+  let attr = get_tok state2.consume()
+  if attr.tokenType != CSS_IDENT_TOKEN: fail
+  state2.skipWhitespace()
+  if not state2.has(): return Selector(t: ATTR_SELECTOR, attr: attr.value, rel: ' ')
+  let delim0 = get_tok state2.consume()
+  if delim0.tokenType != CSS_DELIM_TOKEN: fail
+  case delim0.rvalue
+  of Rune('~'), Rune('|'), Rune('^'), Rune('$'), Rune('*'):
+    let delim1 = get_tok state2.consume()
+    if delim1.tokenType != CSS_DELIM_TOKEN: fail
+  of Rune('='):
+    discard
+  else: fail
+  state2.skipWhitespace()
+  if not state2.has(): fail
+  let value = get_tok state2.consume()
+  if value.tokenType notin {CSS_IDENT_TOKEN, CSS_STRING_TOKEN}: fail
+  return Selector(t: ATTR_SELECTOR, attr: attr.value, value: value.value, rel: cast[char](delim0.rvalue))
+
+proc parseClassSelector(state: var SelectorParser): Selector =
+  if not state.has(): fail
+  let tok = get_tok state.consume()
+  if tok.tokenType != CSS_IDENT_TOKEN: fail
+  return Selector(t: CLASS_SELECTOR, class: tok.value)
+
+proc parseCompoundSelector(state: var SelectorParser): CompoundSelector =
+  while state.has():
+    let cval = state.peek()
     if cval of CSSToken:
-      let csstoken = (CSSToken)cval
-      case csstoken.tokenType
+      let tok = CSSToken(cval)
+      case tok.tokenType
       of CSS_IDENT_TOKEN:
-        case state.query
-        of QUERY_ATTR:
-          state.query = QUERY_DELIM
-          state.addSelector(Selector(t: ATTR_SELECTOR, attr: csstoken.value, rel: ' '))
-        of QUERY_VALUE:
-          state.getLastSel().value = csstoken.value
-          break
-        else: discard
-      of CSS_STRING_TOKEN:
-        case state.query
-        of QUERY_VALUE:
-          state.getLastSel().value = csstoken.value
-          break
-        else: discard
+        inc state.at
+        result.add(Selector(t: TYPE_SELECTOR, tag: tagType(tok.value)))
+      of CSS_COLON_TOKEN:
+        inc state.at
+        result.add(state.parsePseudoSelector())
+      of CSS_HASH_TOKEN:
+        inc state.at
+        result.add(Selector(t: ID_SELECTOR, id: tok.value))
+      of CSS_COMMA_TOKEN: break
       of CSS_DELIM_TOKEN:
-        case csstoken.rvalue
-        of Rune('~'), Rune('|'), Rune('^'), Rune('$'), Rune('*'):
-          if state.query == QUERY_DELIM:
-            state.getLastSel().rel = char(csstoken.rvalue)
-        of Rune('='):
-          if state.query == QUERY_DELIM:
-            if state.getLastSel().rel == ' ':
-              state.getLastSel().rel = '='
-            state.query = QUERY_VALUE
-        else: discard
-      else: discard
-  state.query = QUERY_TYPE
-
-proc parseSelectorFunction(state: var SelectorParser, cssfunction: CSSFunction)
-
-proc parseSelectorFunctionBody(state: var SelectorParser, body: seq[CSSComponentValue]): seq[ComplexSelector] =
-  let osels = state.selectors
-  let ocomb = state.combinator
-  state.combinator = nil
-  state.selectors = newSeq[ComplexSelector]()
-  state.addComplexSelector()
-  for cval in body:
-    if state.query == QUERY_FAILED:
-      break
-    if cval of CSSToken:
-      state.parseSelectorToken(CSSToken(cval))
+        case tok.rvalue
+        of Rune('.'):
+          inc state.at
+          result.add(state.parseClassSelector())
+        of Rune('*'):
+          inc state.at
+          result.add(Selector(t: UNIVERSAL_SELECTOR))
+        of Rune('>'), Rune('+'), Rune('~'): break
+        else: fail
+      of CSS_WHITESPACE_TOKEN:
+        # skip trailing whitespace
+        if not state.has(1) or state.peek(1) == CSS_COMMA_TOKEN:
+          inc state.at
+        elif state.peek(1) == CSS_DELIM_TOKEN:
+          let tok = CSSToken(state.peek(1))
+          if tok.rvalue == Rune('>') or tok.rvalue == Rune('+') or tok.rvalue == Rune('~'):
+            inc state.at
+        break
+      else: fail
     elif cval of CSSSimpleBlock:
-      state.parseSelectorSimpleBlock(CSSSimpleBlock(cval))
-    elif cval of CSSFunction:
-      state.parseSelectorFunction(CSSFunction(cval))
-  if state.query == QUERY_FAILED:
-    state.selectors = @[]
-    state.combinator = nil
-  else:
-    result = state.getComplexSelectors()
-    state.selectors = osels
-    state.combinator = ocomb
-
-proc parseNthChild(state: var SelectorParser, cssfunction: CSSFunction, data: PseudoData) =
-  var data = data
-  let (anb, i) = parseAnB(cssfunction.value)
-  if anb.isSome:
-    data.anb = anb.get
-    var nthchild = Selector(t: PSEUDO_SELECTOR, pseudo: data)
-    var i = i
-    while i < cssfunction.value.len and cssfunction.value[i] == CSS_WHITESPACE_TOKEN:
-      inc i
-    if i >= cssfunction.value.len:
-      state.addSelector(nthchild)
+      inc state.at
+      result.add(state.parseAttributeSelector(CSSSimpleBlock(cval)))
     else:
-      if cssfunction.value[i] == CSS_IDENT_TOKEN and CSSToken(cssfunction.value[i]).value == "of":
-        if i < cssfunction.value.len:
-          let body = cssfunction.value[i..^1]
-          let val = state.parseSelectorFunctionBody(body)
-          if val.len > 0:
-            nthchild.pseudo.ofsels = some(val)
-            state.addSelector(nthchild)
-  state.query = QUERY_TYPE
-
-proc parseLang(state: var SelectorParser, body: seq[CSSComponentValue]) =
-  if body.len == 0:
-    state.query = QUERY_FAILED
-    return
-  var i = 0
-  template tok: CSSComponentValue = body[i]
-  while i < body.len:
-    if tok != CSS_WHITESPACE_TOKEN: break
-    inc i
-  if i >= body.len:
-    state.query = QUERY_FAILED
-    return
-  if tok != CSS_IDENT_TOKEN: return
-  state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: PseudoData(t: PSEUDO_LANG, s: CSSToken(tok).value)))
-
-# Functions that may contain other selectors, functions, etc.
-proc parseRecursiveSelectorFunction(state: var SelectorParser, class: PseudoClass, body: seq[CSSComponentValue]) =
-  state.query = QUERY_TYPE
-  var data = PseudoData(t: class)
-  var fun = Selector(t: PSEUDO_SELECTOR, pseudo: data)
-  state.addSelector(fun)
-  fun.pseudo.fsels = state.parseSelectorFunctionBody(body)
-
-proc parseSelectorFunction(state: var SelectorParser, cssfunction: CSSFunction) =
-  if state.query != QUERY_PSEUDO:
-    state.query = QUERY_FAILED
-    return
-  case cssfunction.name
-  of "not":
-    state.parseRecursiveSelectorFunction(PSEUDO_NOT, cssfunction.value)
-  of "is":
-    state.parseRecursiveSelectorFunction(PSEUDO_IS, cssfunction.value)
-  of "where":
-    state.parseRecursiveSelectorFunction(PSEUDO_WHERE, cssfunction.value)
-  of "nth-child":
-    state.parseNthChild(cssfunction, PseudoData(t: PSEUDO_NTH_CHILD))
-  of "nth-last-child":
-    state.parseNthChild(cssfunction, PseudoData(t: PSEUDO_NTH_LAST_CHILD))
-  of "lang":
-    state.parseLang(cssfunction.value)
-  else:
-    state.query = QUERY_FAILED
+      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()
+    var cxsel: ComplexSelector
+    case tok.tokenType
+    of CSS_DELIM_TOKEN:
+      case tok.rvalue
+      of Rune('>'): result[^1].ct = CHILD_COMBINATOR
+      of Rune('+'): result[^1].ct = NEXT_SIBLING_COMBINATOR
+      of Rune('~'): result[^1].ct = SUBSEQ_SIBLING_COMBINATOR
+      else: fail
+    of CSS_WHITESPACE_TOKEN:
+      result[^1].ct = DESCENDANT_COMBINATOR
+    of CSS_COMMA_TOKEN:
+      break # finish
+    else: fail
+  if result.len == 0 or result[^1].ct != NO_COMBINATOR:
+    fail
+
+proc parseSelectorList(cvals: seq[CSSComponentValue]): SelectorList =
+  var state = SelectorParser(cvals: cvals)
+  var res: SelectorList
+  while state.has():
+    res.add(state.parseComplexSelector())
+  if not state.failed:
+    return res
 
 func parseSelectors*(cvals: seq[CSSComponentValue]): seq[ComplexSelector] = {.cast(noSideEffect).}:
-  var state = SelectorParser()
-  state.addComplexSelector()
-  for cval in cvals:
-    if state.query == QUERY_FAILED:
-      break
-    if cval of CSSToken:
-      state.parseSelectorToken(CSSToken(cval))
-    elif cval of CSSSimpleBlock:
-      state.parseSelectorSimpleBlock(CSSSimpleBlock(cval))
-    elif cval of CSSFunction:
-      state.parseSelectorFunction(CSSFunction(cval))
-  state.flushCombinator()
-  if state.selectors.len > 0 and state.selectors[^1].len == 0:
-    state.query = QUERY_FAILED
-  if state.query == QUERY_FAILED:
-    return @[]
-  return state.selectors
+  return parseSelectorList(cvals)
 
 proc parseSelectors*(stream: Stream): seq[ComplexSelector] =
   return parseSelectors(parseListOfComponentValues(stream))
diff --git a/src/css/sheet.nim b/src/css/sheet.nim
index 846f6b31..d1b47f1c 100644
--- a/src/css/sheet.nim
+++ b/src/css/sheet.nim
@@ -39,6 +39,19 @@ func newStylesheet*(cap: int): CSSStylesheet =
   result.class_table = newTable[string, seq[CSSRuleDef]](bucketsize)
   result.general_list = newSeqOfCap[CSSRuleDef](bucketsize)
 
+proc getSelectorIds(hashes: var SelectorHashes, sel: Selector): bool
+
+proc getSelectorIds(hashes: var SelectorHashes, sels: CompoundSelector) =
+  for sel in sels:
+    if hashes.getSelectorIds(sel):
+      break
+
+# For now, we match elements against the *last* selector.
+#TODO this is inefficient, so we should eventually get rid of this
+# function
+proc getSelectorIds(hashes: var SelectorHashes, cxsel: ComplexSelector) =
+  hashes.getSelectorIds(cxsel[^1])
+
 proc getSelectorIds(hashes: var SelectorHashes, sel: Selector): bool =
   case sel.t
   of TYPE_SELECTOR:
@@ -52,11 +65,6 @@ proc getSelectorIds(hashes: var SelectorHashes, sel: Selector): bool =
     return true
   of ATTR_SELECTOR, PSELEM_SELECTOR, UNIVERSAL_SELECTOR:
     return false
-  of COMBINATOR_SELECTOR:
-    for sel in sel.csels[^1]:
-      if hashes.getSelectorIds(sel):
-        return true
-    return false
   of PSEUDO_SELECTOR:
     if sel.pseudo.t in {PSEUDO_IS, PSEUDO_WHERE}:
       # Basically just hash whatever the selectors have in common:
@@ -71,18 +79,12 @@ proc getSelectorIds(hashes: var SelectorHashes, sel: Selector): bool =
       var cancel_class = false
       var i = 0
       if i < sel.pseudo.fsels.len:
-        let list = sel.pseudo.fsels[i]
-        for sel in list:
-          if hashes.getSelectorIds(sel):
-            break
+        hashes.getSelectorIds(sel.pseudo.fsels[i])
         inc i
 
       while i < sel.pseudo.fsels.len:
-        let list = sel.pseudo.fsels[i]
         var nhashes: SelectorHashes
-        for sel in list:
-          if nhashes.getSelectorIds(sel):
-            break
+        nhashes.getSelectorIds(sel.pseudo.fsels[i])
         if hashes.tag == TAG_UNKNOWN:
           hashes.tag = nhashes.tag
         elif not cancel_tag and nhashes.tag != TAG_UNKNOWN and nhashes.tag != hashes.tag:
@@ -127,11 +129,8 @@ iterator gen_rules*(sheet: CSSStylesheet, tag: TagType, id: string, classes: seq
 
 proc add(sheet: var CSSStylesheet, rule: CSSRuleDef) =
   var hashes: SelectorHashes
-  for list in rule.sels:
-    for sel in list:
-      if hashes.getSelectorIds(sel):
-        break
-
+  for cxsel in rule.sels:
+    hashes.getSelectorIds(cxsel)
     if hashes.tag != TAG_UNKNOWN:
       sheet.tag_table[hashes.tag].add(rule)
     elif hashes.id != "":
@@ -171,7 +170,7 @@ proc getDeclarations(rule: CSSQualifiedRule): seq[CSSDeclaration] {.inline.} =
 
 proc addRule(stylesheet: var CSSStylesheet, rule: CSSQualifiedRule) =
   let sels = parseSelectors(rule.prelude)
-  if sels.len > 0 and sels[^1].len > 0:
+  if sels.len > 0:
     let r = CSSRuleDef(sels: sels, decls: rule.getDeclarations())
     stylesheet.add(r)
 
diff --git a/src/css/stylednode.nim b/src/css/stylednode.nim
index c18c8779..0eac3c66 100644
--- a/src/css/stylednode.nim
+++ b/src/css/stylednode.nim
@@ -93,6 +93,10 @@ func findElement*(root: StyledNode, elem: Element): StyledNode =
 func isDomElement*(styledNode: StyledNode): bool {.inline.} =
   styledNode.t == STYLED_ELEMENT and styledNode.pseudo == PSEUDO_NONE
 
+# DOM-style getters, for Element interoperability...
+func parentElement*(node: StyledNode): StyledNode {.inline.} =
+  node.parent
+
 func checked(element: Element): bool =
   if element.tagType == TAG_INPUT:
     let input = HTMLInputElement(element)