about summary refs log tree commit diff stats
path: root/src/css
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2023-01-23 01:02:43 +0100
committerbptato <nincsnevem662@gmail.com>2023-01-27 17:41:57 +0100
commit167dd67270d5a432c584b61f5a22281ca47017d9 (patch)
tree4603f59617718cf255ce0ef562e85c530ed0137f /src/css
parent11271f01439f2593a79501e83b13688e032fe7ed (diff)
downloadchawan-167dd67270d5a432c584b61f5a22281ca47017d9.tar.gz
WIP selector rewrite
pretty slow for some reason
Diffstat (limited to 'src/css')
-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)
99'>299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538