about summary refs log tree commit diff stats
path: root/src/css
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2021-11-23 12:25:42 +0100
committerbptato <nincsnevem662@gmail.com>2021-11-23 12:25:42 +0100
commitff1b68086d699510dcdbea6051460926556bd401 (patch)
treef4d7419534fbf7c960b704985ad27cb2e807a7c4 /src/css
parentfae6b15f8fe7d59caa61b2295e6b71d89b70a795 (diff)
downloadchawan-ff1b68086d699510dcdbea6051460926556bd401.tar.gz
Support CSS descendant combinators
Diffstat (limited to 'src/css')
-rw-r--r--src/css/selector.nim94
-rw-r--r--src/css/style.nim46
2 files changed, 108 insertions, 32 deletions
diff --git a/src/css/selector.nim b/src/css/selector.nim
index 0ec1ac27..97c7d695 100644
--- a/src/css/selector.nim
+++ b/src/css/selector.nim
@@ -7,22 +7,26 @@ import css/parser
 type
   SelectorType* = enum
     TYPE_SELECTOR, ID_SELECTOR, ATTR_SELECTOR, CLASS_SELECTOR,
-    UNIVERSAL_SELECTOR, PSEUDO_SELECTOR, PSELEM_SELECTOR, FUNC_SELECTOR
+    UNIVERSAL_SELECTOR, PSEUDO_SELECTOR, PSELEM_SELECTOR, FUNC_SELECTOR,
+    COMBINATOR_SELECTOR
 
   QueryMode* = enum
     QUERY_TYPE, QUERY_CLASS, QUERY_ATTR, QUERY_DELIM, QUERY_VALUE,
-    QUERY_PSEUDO, QUERY_PSELEM
+    QUERY_PSEUDO, QUERY_PSELEM, QUERY_COMBINATOR
 
   PseudoElem* = enum
     PSEUDO_NONE, PSEUDO_BEFORE, PSEUDO_AFTER
 
+  CombinatorType* = enum
+    DESCENDANT_COMBINATOR
+
   SelectorParser = object
     selectors: seq[SelectorList]
     query: QueryMode
-    negate: bool
+    combinator: Selector
 
   #TODO combinators
-  Selector* = object
+  Selector* = ref object of RootObj
     case t*: SelectorType
     of TYPE_SELECTOR:
       tag*: TagType
@@ -42,7 +46,10 @@ type
       elem*: string
     of FUNC_SELECTOR:
       name*: string
-      selectors*: SelectorList
+      fsels*: SelectorList
+    of COMBINATOR_SELECTOR:
+      ct*: CombinatorType
+      csels*: seq[SelectorList]
 
   SelectorList* = ref object
     sels*: seq[Selector]
@@ -54,6 +61,8 @@ proc setLen*(sellist: SelectorList, i: int) = sellist.sels.setLen(i)
 proc `[]`*(sellist: SelectorList, i: int): Selector = sellist.sels[i]
 proc len*(sellist: SelectorList): int = sellist.sels.len
 
+func getSpecificity*(sels: SelectorList): int
+
 func getSpecificity(sel: Selector): int =
   case sel.t
   of ID_SELECTOR:
@@ -66,17 +75,21 @@ func getSpecificity(sel: Selector): int =
     case sel.name
     of "is":
       var best = 0
-      for child in sel.selectors.sels:
+      for child in sel.fsels.sels:
         let s = getSpecificity(child)
         if s > best:
           best = s
       result += best
     of "not":
-      for child in sel.selectors.sels:
-        result += getSpecificity(child)
+      result += getSpecificity(sel.fsels)
     else: discard
   of UNIVERSAL_SELECTOR:
     discard
+  of COMBINATOR_SELECTOR:
+    case sel.ct
+    of DESCENDANT_COMBINATOR:
+      for child in sel.csels:
+        result += getSpecificity(child)
 
 func getSpecificity*(sels: SelectorList): int =
   for sel in sels.sels:
@@ -110,28 +123,54 @@ func optimizeSelectorList*(selectors: SelectorList): SelectorList =
   else:
     result.add(selectors[0])
 
+proc addSelector(state: var SelectorParser, sel: Selector) =
+  if state.combinator != nil:
+    state.combinator.csels[^1].add(sel)
+  else:
+    state.selectors[^1].add(sel)
+
+proc addSelectorList(state: var SelectorParser) =
+  if state.combinator != nil:
+    state.selectors[^1].add(state.combinator)
+    state.combinator = nil
+  state.selectors.add(SelectorList())
+
 proc parseSelectorToken(state: var SelectorParser, csstoken: CSSToken) =
+  if state.query == QUERY_COMBINATOR:
+    if csstoken.tokenType in {CSS_IDENT_TOKEN, CSS_HASH_TOKEN,
+                              CSS_COLON_TOKEN}:
+      if state.combinator == nil:
+        state.combinator = Selector(t: COMBINATOR_SELECTOR, ct: DESCENDANT_COMBINATOR)
+      state.combinator.csels.add(state.selectors[^1])
+      if state.combinator.csels[^1].len > 0:
+        state.combinator.csels.add(SelectorList())
+      state.selectors[^1] = SelectorList()
+      state.query = QUERY_TYPE
+
   case csstoken.tokenType
   of CSS_IDENT_TOKEN:
     case state.query
     of QUERY_CLASS:
-      state.selectors[^1].add(Selector(t: CLASS_SELECTOR, class: $csstoken.value))
+      state.addSelector(Selector(t: CLASS_SELECTOR, class: $csstoken.value))
     of QUERY_TYPE:
-      state.selectors[^1].add(Selector(t: TYPE_SELECTOR, tag: tagType($csstoken.value)))
+      state.addSelector(Selector(t: TYPE_SELECTOR, tag: tagType($csstoken.value)))
     of QUERY_PSEUDO:
-      state.selectors[^1].add(Selector(t: PSEUDO_SELECTOR, pseudo: $csstoken.value))
+      state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: $csstoken.value))
     of QUERY_PSELEM:
-      state.selectors[^1].add(Selector(t: PSELEM_SELECTOR, elem: $csstoken.value))
+      state.addSelector(Selector(t: PSELEM_SELECTOR, elem: $csstoken.value))
     else: discard
     state.query = QUERY_TYPE
   of CSS_DELIM_TOKEN:
     if csstoken.rvalue == Rune('.'):
       state.query = QUERY_CLASS
   of CSS_HASH_TOKEN:
-    state.selectors[^1].add(Selector(t: ID_SELECTOR, id: $csstoken.value))
+    state.addSelector(Selector(t: ID_SELECTOR, id: $csstoken.value))
   of CSS_COMMA_TOKEN:
     if state.selectors[^1].len > 0:
-      state.selectors.add(SelectorList())
+      state.addSelectorList()
+  of CSS_WHITESPACE_TOKEN:
+    if state.selectors[^1].len > 0 or state.combinator != nil:
+      state.query = QUERY_COMBINATOR
   of CSS_COLON_TOKEN:
     if state.query == QUERY_PSEUDO:
       state.query = QUERY_PSELEM
@@ -151,7 +190,7 @@ proc parseSelectorSimpleBlock(state: var SelectorParser, cssblock: CSSSimpleBloc
           case state.query
           of QUERY_ATTR:
             state.query = QUERY_DELIM
-            state.selectors[^1].add(Selector(t: ATTR_SELECTOR, attr: $csstoken.value, rel: ' '))
+            state.addSelector(Selector(t: ATTR_SELECTOR, attr: $csstoken.value, rel: ' '))
           of QUERY_VALUE:
             state.selectors[^1].sels[^1].value = $csstoken.value
             break
@@ -183,26 +222,29 @@ proc parseSelectorFunction(state: var SelectorParser, cssfunction: CSSFunction)
     state.query = QUERY_TYPE
   else: return
   var fun = Selector(t: FUNC_SELECTOR, name: $cssfunction.name)
-  fun.selectors = SelectorList(parent: state.selectors[^1])
-  state.selectors[^1].add(fun)
-  state.selectors[^1] = fun.selectors
+  fun.fsels = SelectorList(parent: state.selectors[^1])
+  state.addSelector(fun)
+  state.selectors[^1] = fun.fsels
   for cval in cssfunction.value:
     if cval of CSSToken:
-      state.parseSelectorToken((CSSToken)cval)
+      state.parseSelectorToken(CSSToken(cval))
     elif cval of CSSSimpleBlock:
-      state.parseSelectorSimpleBlock((CSSSimpleBlock)cval)
+      state.parseSelectorSimpleBlock(CSSSimpleBlock(cval))
     elif cval of CSSFunction:
-      state.parseSelectorFunction((CSSFunction)cval)
-  state.selectors[^1] = fun.selectors.parent
+      state.parseSelectorFunction(CSSFunction(cval))
+  state.selectors[^1] = fun.fsels.parent
 
 func parseSelectors*(cvals: seq[CSSComponentValue]): seq[SelectorList] =
   var state = SelectorParser()
-  state.selectors.add(SelectorList())
+  state.addSelectorList()
   for cval in cvals:
     if cval of CSSToken:
-      state.parseSelectorToken((CSSToken)cval)
+      state.parseSelectorToken(CSSToken(cval))
     elif cval of CSSSimpleBlock:
-      state.parseSelectorSimpleBlock((CSSSimpleBlock)cval)
+      state.parseSelectorSimpleBlock(CSSSimpleBlock(cval))
     elif cval of CSSFunction:
-      state.parseSelectorFunction((CSSFunction)cval)
+      state.parseSelectorFunction(CSSFunction(cval))
+  if state.combinator != nil:
+    state.selectors[^1].add(state.combinator)
+    state.combinator = nil
   return state.selectors
diff --git a/src/css/style.nim b/src/css/style.nim
index 22466d20..897c1652 100644
--- a/src/css/style.nim
+++ b/src/css/style.nim
@@ -50,6 +50,8 @@ func pseudoElemSelectorMatches(elem: Element, sel: Selector): SelectResult =
   of "before": return selectres(true, PSEUDO_AFTER)
   else: return selectres(false)
 
+func selectorsMatch(elem: Element, selectors: SelectorList): SelectResult
+
 func selectorMatches(elem: Element, sel: Selector): SelectResult =
   case sel.t
   of TYPE_SELECTOR:
@@ -68,6 +70,29 @@ func selectorMatches(elem: Element, sel: Selector): SelectResult =
     return selectres(true)
   of FUNC_SELECTOR:
     return selectres(false)
+  of COMBINATOR_SELECTOR:
+    case sel.ct
+    of DESCENDANT_COMBINATOR:
+      #combinator without at least two members makes no sense
+      assert sel.csels.len > 1
+      if elem.selectorsMatch(sel.csels[^1]).success:
+        var i = sel.csels.len - 2
+        var e = elem.parentElement
+        var pseudo = PSEUDO_NONE
+        while e != nil and e != elem.ownerDocument.root and i >= 0:
+          let res = e.selectorsMatch(sel.csels[i])
+
+          if res.pseudo != PSEUDO_NONE:
+            if pseudo != PSEUDO_NONE:
+              return selectres(false)
+            pseudo = res.pseudo
+
+          if res.success:
+            dec i
+          e = e.parentElement
+        return selectres(i == -1, pseudo)
+      else:
+        return selectres(false)
 
 func selectorsMatch(elem: Element, selectors: SelectorList): SelectResult =
   for sel in selectors.sels:
@@ -99,10 +124,13 @@ func selectElems(document: Document, sel: Selector): seq[Element] =
   of FUNC_SELECTOR:
     case sel.name
     of "not":
-      return document.all_elements.filter((elem) => not selectorsMatch(elem, sel.selectors).psuccess)
+      return document.all_elements.filter((elem) => not selectorsMatch(elem, sel.fsels).psuccess)
     of "is", "where":
-      return document.all_elements.filter((elem) => selectorsMatch(elem, sel.selectors).psuccess)
+      return document.all_elements.filter((elem) => selectorsMatch(elem, sel.fsels).psuccess)
     return newSeq[Element]()
+  of COMBINATOR_SELECTOR:
+    #TODO
+    return document.all_elements.filter((elem) => selectorMatches(elem, sel))
 
 func selectElems(document: Document, selectors: SelectorList): seq[Element] =
   assert(selectors.len > 0)
@@ -114,9 +142,9 @@ func selectElems(document: Document, selectors: SelectorList): seq[Element] =
     if sellist[i].t == FUNC_SELECTOR:
       case sellist[i].name
       of "not":
-        result = result.filter((elem) => not selectorsMatch(elem, sellist[i].selectors).psuccess)
+        result = result.filter((elem) => not selectorsMatch(elem, sellist[i].fsels).psuccess)
       of "is", "where":
-        result = result.filter((elem) => selectorsMatch(elem, sellist[i].selectors).psuccess)
+        result = result.filter((elem) => selectorsMatch(elem, sellist[i].fsels).psuccess)
       else: discard
     else:
       result = result.filter((elem) => selectorMatches(elem, sellist[i]).psuccess)
@@ -148,6 +176,12 @@ type
     normal: seq[tuple[e:Element,d:CSSDeclaration,p:PseudoElem]]
     important: seq[tuple[e:Element,d:CSSDeclaration,p:PseudoElem]]
 
+proc parseStylesheet*(s: Stream): ParsedStylesheet =
+  for v in parseCSS(s).value:
+    let sels = parseSelectors(v.prelude)
+    if sels.len > 1 or sels[^1].len > 0:
+      result.add((sels: sels, oblock: v.oblock))
+
 func calcRules(elem: Element, rules: ParsedStylesheet):
     array[low(PseudoElem)..high(PseudoElem), seq[CSSSimpleBlock]] =
   var tosorts: array[low(PseudoElem)..high(PseudoElem), seq[tuple[s:int,b:CSSSimpleBlock]]]
@@ -211,7 +245,7 @@ proc applyAuthorRules*(document: Document): ApplyResult =
   stack.add(document.body)
 
   if rules_head.len > 0:
-    let parsed = parseCSS(newStringStream(rules_head)).value.map((x) => (sels: parseSelectors(x.prelude), oblock: x.oblock))
+    let parsed = newStringStream(rules_head).parseStylesheet()
     embedded_rules.add(parsed)
 
   while stack.len > 0:
@@ -224,7 +258,7 @@ proc applyAuthorRules*(document: Document): ApplyResult =
             rules_local &= Text(ct).data
 
     if rules_local.len > 0:
-      let parsed = parseCSS(newStringStream(rules_local)).value.map((x) => (sels: parseSelectors(x.prelude), oblock: x.oblock))
+      let parsed = newStringStream(rules_local).parseStylesheet()
       embedded_rules.add(parsed)
 
     if not elem.cssapplied: