about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2025-01-14 18:59:17 +0100
committerbptato <nincsnevem662@gmail.com>2025-01-14 21:48:20 +0100
commit99b72030569ae2f688002bd71d1fb58ad6daec90 (patch)
tree07846b88ac6016cca4f0ae3638c91f48ad40e7a7 /src
parentb88ff4a313e04632c41d7ef90370599484a6f0bc (diff)
downloadchawan-99b72030569ae2f688002bd71d1fb58ad6daec90.tar.gz
css: refactor selector parsing & cascading, some work on variables
Untangled some nested arrays to reduce the number of intermediary seqs
in cascade, and collapsed the two rule def sorts into just one (per
pseudo element).  This should make cascading somewhat faster.

Also, we now parse variables, but they aren't resolved yet.
Probably a seq won't cut it for var lookup...
Diffstat (limited to 'src')
-rw-r--r--src/css/cascade.nim215
-rw-r--r--src/css/cssvalues.nim105
-rw-r--r--src/css/selectorparser.nim76
-rw-r--r--src/css/sheet.nim46
-rw-r--r--src/css/stylednode.nim8
-rw-r--r--src/html/dom.nim10
6 files changed, 258 insertions, 202 deletions
diff --git a/src/css/cascade.nim b/src/css/cascade.nim
index aaf68677..b2eef00e 100644
--- a/src/css/cascade.nim
+++ b/src/css/cascade.nim
@@ -21,23 +21,33 @@ import types/opt
 import types/winattrs
 
 type
-  RuleList* = array[PseudoElem, seq[CSSRuleDef]]
+  RuleListEntry = object
+    normal: seq[CSSComputedEntry]
+    important: seq[CSSComputedEntry]
+
+  RuleList = array[CSSOrigin, RuleListEntry]
+
+  RuleListMap = ref object
+    rules: array[PseudoElement, RuleList]
 
-  RuleListMap* = ref object
-    ua: RuleList # user agent
-    user: RuleList
-    author: seq[RuleList]
+  RulePair = tuple
+    specificity: int
+    rule: CSSRuleDef
 
-  ToSorts = array[PseudoElem, seq[(int, CSSRuleDef)]]
+  ToSorts = array[PseudoElement, seq[RulePair]]
 
 proc calcRule(tosorts: var ToSorts; element: Element;
     depends: var DependencyInfo; rule: CSSRuleDef) =
   for sel in rule.sels:
     if element.matches(sel, depends):
-      let spec = getSpecificity(sel)
-      tosorts[sel.pseudo].add((spec, rule))
+      if tosorts[sel.pseudo].len > 0 and tosorts[sel.pseudo][^1].rule == rule:
+        tosorts[sel.pseudo][^1].specificity =
+          max(tosorts[sel.pseudo][^1].specificity, sel.specificity)
+      else:
+        tosorts[sel.pseudo].add((sel.specificity, rule))
 
-func calcRules(styledNode: StyledNode; sheet: CSSStylesheet): RuleList =
+func calcRules(map: RuleListMap; styledNode: StyledNode; sheet: CSSStylesheet;
+    origin: CSSOrigin) =
   var tosorts = ToSorts.default
   let element = Element(styledNode.node)
   var rules: seq[CSSRuleDef] = @[]
@@ -54,27 +64,28 @@ func calcRules(styledNode: StyledNode; sheet: CSSStylesheet): RuleList =
       rules.add(v[])
   for rule in sheet.generalList:
     rules.add(rule)
-  rules.sort(ruleDefCmp, order = Ascending)
   for rule in rules:
     tosorts.calcRule(element, styledNode.depends, rule)
-  for i in PseudoElem:
-    tosorts[i].sort((proc(x, y: (int, CSSRuleDef)): int =
-      cmp(x[0], y[0])
-    ), order = Ascending)
-    result[i] = newSeqOfCap[CSSRuleDef](tosorts[i].len)
-    for item in tosorts[i]:
-      result[i].add(item[1])
+  for pseudo, it in tosorts.mpairs:
+    it.sort(proc(x, y: (int, CSSRuleDef)): int =
+      let n = cmp(x[0], y[0])
+      if n != 0:
+        return n
+      return cmp(x[1].idx, y[1].idx), order = Ascending)
+    for item in it:
+      map.rules[pseudo][origin].normal.add(item[1].normalVals)
+      map.rules[pseudo][origin].important.add(item[1].importantVals)
 
 proc applyPresHints(computed: CSSValues; element: Element;
     attrs: WindowAttributes; initMap: var InitMap) =
   template set_cv(t, x, b: untyped) =
-    computed.applyValue(makeEntry(t, CSSValueWord(x: b)), nil, nil, initMap,
-      itUserAgent)
+    computed.applyValue(makeEntry(t, CSSValueWord(x: b)), nil, nil, nil,
+      initMap, itUserAgent)
     initMap[t].incl(itUser)
   template set_cv_new(t, x, b: untyped) =
     const v = valueType(t)
     let val = CSSValue(v: v, x: b)
-    computed.applyValue(makeEntry(t, val), nil, nil, initMap, itUserAgent)
+    computed.applyValue(makeEntry(t, val), nil, nil, nil, initMap, itUserAgent)
     initMap[t].incl(itUser)
   template map_width =
     let s = parseDimensionValues(element.attr(satWidth))
@@ -129,7 +140,7 @@ proc applyPresHints(computed: CSSValues; element: Element;
   template set_bgcolor_is_canvas =
     let t = cptBgcolorIsCanvas
     let val = CSSValueBit(bgcolorIsCanvas: true)
-    computed.applyValue(makeEntry(t, val), nil, nil, initMap, itUserAgent)
+    computed.applyValue(makeEntry(t, val), nil, nil, nil, initMap, itUserAgent)
     initMap[t].incl(itUser)
   template map_cellspacing =
     let s = element.attrul(satCellspacing)
@@ -185,42 +196,34 @@ proc applyPresHints(computed: CSSValues; element: Element;
       set_cv cptHeight, length, resolveLength(cuEm, float32(size), attrs)
   else: discard
 
-type
-  CSSValueEntryObj = object
-    normal: seq[CSSComputedEntry]
-    important: seq[CSSComputedEntry]
-
-  CSSValueEntryMap = array[CSSOrigin, CSSValueEntryObj]
-
-# element and attrsp are nil if called for a pseudo element.
-func buildComputedValues(rules: CSSValueEntryMap; parent: CSSValues;
-    element: Element; attrsp: ptr WindowAttributes): CSSValues =
+func applyDeclarations0(rules: RuleList; parent: CSSValues; element: Element;
+    window: Window): CSSValues =
   result = CSSValues()
   var initMap = InitMap.default
   for entry in rules[coUserAgent].normal: # user agent
-    result.applyValue(entry, parent, nil, initMap, itOther)
+    result.applyValue(entry, nil, parent, nil, initMap, itOther)
     initMap[entry.t] = {itUserAgent, itUser}
   let uaProperties = result.copyProperties()
   # Presentational hints override user agent style, but respect user/author
   # style.
   if element != nil:
-    result.applyPresHints(element, attrsp[], initMap)
+    result.applyPresHints(element, window.attrsp[], initMap)
   for entry in rules[coUser].normal: # user
-    result.applyValue(entry, parent, uaProperties, initMap, itUserAgent)
+    result.applyValue(entry, nil, parent, uaProperties, initMap, itUserAgent)
     initMap[entry.t].incl(itUser)
   # save user properties so author can use them
   let userProperties = result.copyProperties()
   for entry in rules[coAuthor].normal: # author
-    result.applyValue(entry, parent, userProperties, initMap, itUser)
+    result.applyValue(entry, nil, parent, userProperties, initMap, itUser)
     initMap[entry.t].incl(itOther)
   for entry in rules[coAuthor].important: # author important
-    result.applyValue(entry, parent, userProperties, initMap, itUser)
+    result.applyValue(entry, nil, parent, userProperties, initMap, itUser)
     initMap[entry.t].incl(itOther)
   for entry in rules[coUser].important: # user important
-    result.applyValue(entry, parent, uaProperties, initMap, itUserAgent)
+    result.applyValue(entry, nil, parent, uaProperties, initMap, itUserAgent)
     initMap[entry.t].incl(itOther)
   for entry in rules[coUserAgent].important: # user agent important
-    result.applyValue(entry, parent, nil, initMap, itUserAgent)
+    result.applyValue(entry, nil, parent, nil, initMap, itUserAgent)
     initMap[entry.t].incl(itOther)
   # set defaults
   for t in CSSPropertyType:
@@ -240,53 +243,21 @@ func buildComputedValues(rules: CSSValueEntryMap; parent: CSSValues;
     result{"overflow-x"} = result{"overflow-x"}.bfcify()
     result{"overflow-y"} = result{"overflow-y"}.bfcify()
 
-proc add(map: var CSSValueEntryObj; rules: seq[CSSRuleDef]) =
-  for rule in rules:
-    map.normal.add(rule.normalVals)
-    map.important.add(rule.importantVals)
-
 proc applyDeclarations(styledNode: StyledNode; parent: CSSValues;
-    map: RuleListMap; window: Window) =
-  var rules: CSSValueEntryMap
-  rules[coUserAgent].add(map.ua[peNone])
-  rules[coUser].add(map.user[peNone])
-  for rule in map.author:
-    rules[coAuthor].add(rule[peNone])
-  var element: Element = nil
-  if styledNode.node != nil:
-    element = Element(styledNode.node)
-    let style = element.cachedStyle
-    if window.styling and style != nil:
-      for decl in style.decls:
-        let vals = parseComputedValues(decl.name, decl.value, window.attrsp[])
-        if decl.important:
-          rules[coAuthor].important.add(vals)
-        else:
-          rules[coAuthor].normal.add(vals)
-  styledNode.computed = rules.buildComputedValues(parent, element,
-    window.attrsp)
+    map: RuleListMap; window: Window; pseudo = peNone) =
+  let element = Element(styledNode.node)
+  styledNode.computed = map.rules[pseudo].applyDeclarations0(parent, element,
+    window)
   if element != nil and window.settings.scripting == smApp:
     element.computed = styledNode.computed
 
-func hasValues(rules: CSSValueEntryMap): bool =
-  for origin in CSSOrigin:
-    if rules[origin].normal.len > 0 or rules[origin].important.len > 0:
-      return true
+func hasValues(map: RuleListMap; pseudo: PseudoElement): bool =
+  for x in map.rules:
+    for y in x:
+      if y.normal.len > 0 or y.important.len > 0:
+        return true
   return false
 
-# Either returns a new styled node or nil.
-proc applyDeclarations(pseudo: PseudoElem; styledParent: StyledNode;
-    map: RuleListMap): StyledNode =
-  var rules: CSSValueEntryMap
-  rules[coUserAgent].add(map.ua[pseudo])
-  rules[coUser].add(map.user[pseudo])
-  for rule in map.author:
-    rules[coAuthor].add(rule[pseudo])
-  if rules.hasValues():
-    let cvals = rules.buildComputedValues(styledParent.computed, nil, nil)
-    return styledParent.newStyledElement(pseudo, cvals)
-  return nil
-
 func applyMediaQuery(ss: CSSStylesheet; window: Window): CSSStylesheet =
   if ss == nil:
     return nil
@@ -298,35 +269,32 @@ func applyMediaQuery(ss: CSSStylesheet; window: Window): CSSStylesheet =
   return res
 
 func calcRules(styledNode: StyledNode; ua, user: CSSStylesheet;
-    author: seq[CSSStylesheet]): RuleListMap =
-  let uadecls = calcRules(styledNode, ua)
-  var userdecls = RuleList.default
+    author: seq[CSSStylesheet]; window: Window): RuleListMap =
+  let map = RuleListMap()
+  map.calcRules(styledNode, ua, coUserAgent)
   if user != nil:
-    userdecls = calcRules(styledNode, user)
-  var authordecls: seq[RuleList] = @[]
+    map.calcRules(styledNode, user, coUser)
   for rule in author:
-    authordecls.add(calcRules(styledNode, rule))
-  return RuleListMap(
-    ua: uadecls,
-    user: userdecls,
-    author: authordecls
-  )
-
-proc applyStyle(parent, styledNode: StyledNode; map: RuleListMap;
-    window: Window) =
-  let parentComputed = if parent != nil:
-    parent.computed
-  else:
-    rootProperties()
-  styledNode.applyDeclarations(parentComputed, map, window)
+    map.calcRules(styledNode, rule, coAuthor)
+  if styledNode.node != nil:
+    let style = Element(styledNode.node).cachedStyle
+    if window.styling and style != nil:
+      for decl in style.decls:
+        let vals = parseComputedValues(decl.name, decl.value, window.attrsp[],
+          window.factory)
+        if decl.important:
+          map.rules[peNone][coAuthor].important.add(vals)
+        else:
+          map.rules[peNone][coAuthor].normal.add(vals)
+  return map
 
 type CascadeFrame = object
   styledParent: StyledNode
   child: Node
-  pseudo: PseudoElem
+  pseudo: PseudoElement
   cachedChild: StyledNode
   cachedChildren: seq[StyledNode]
-  parentDeclMap: RuleListMap
+  parentMap: RuleListMap
 
 proc getAuthorSheets(document: Document): seq[CSSStylesheet] =
   var author: seq[CSSStylesheet] = @[]
@@ -350,7 +318,7 @@ proc applyRulesFrameValid(frame: var CascadeFrame): StyledNode =
   return cachedChild
 
 proc applyRulesFrameInvalid(frame: CascadeFrame; ua, user: CSSStylesheet;
-    author: seq[CSSStylesheet]; declmap: var RuleListMap; window: Window):
+    author: seq[CSSStylesheet]; map: var RuleListMap; window: Window):
     StyledNode =
   var styledChild: StyledNode = nil
   let pseudo = frame.pseudo
@@ -359,13 +327,15 @@ proc applyRulesFrameInvalid(frame: CascadeFrame; ua, user: CSSStylesheet;
   if frame.pseudo != peNone:
     case pseudo
     of peBefore, peAfter:
-      let declmap = frame.parentDeclMap
-      let styledPseudo = pseudo.applyDeclarations(styledParent, declmap)
-      if styledPseudo != nil and styledPseudo.computed{"content"}.len > 0:
-        for content in styledPseudo.computed{"content"}:
-          let child = styledPseudo.newStyledReplacement(content, peNone)
-          styledPseudo.children.add(child)
-        styledParent.children.add(styledPseudo)
+      let map = frame.parentMap
+      if map.hasValues(pseudo):
+        let styledPseudo = styledParent.newStyledElement(pseudo)
+        styledPseudo.applyDeclarations(styledParent.computed, map, nil, pseudo)
+        if styledPseudo.computed{"content"}.len > 0:
+          for content in styledPseudo.computed{"content"}:
+            let child = styledPseudo.newStyledReplacement(content, peNone)
+            styledPseudo.children.add(child)
+          styledParent.children.add(styledPseudo)
     of peInputText:
       let s = HTMLInputElement(styledParent.node).inputString()
       if s.len > 0:
@@ -429,8 +399,8 @@ proc applyRulesFrameInvalid(frame: CascadeFrame; ua, user: CSSStylesheet;
         let element = Element(child)
         styledChild = styledParent.newStyledElement(element)
         styledParent.children.add(styledChild)
-        declmap = styledChild.calcRules(ua, user, author)
-        styledParent.applyStyle(styledChild, declmap, window)
+        map = styledChild.calcRules(ua, user, author, window)
+        styledChild.applyDeclarations(styledParent.computed, map, window)
       elif child of Text:
         let text = Text(child)
         styledChild = styledParent.newStyledText(text)
@@ -439,8 +409,8 @@ proc applyRulesFrameInvalid(frame: CascadeFrame; ua, user: CSSStylesheet;
       # Root element
       let element = Element(child)
       styledChild = newStyledElement(element)
-      declmap = styledChild.calcRules(ua, user, author)
-      styledParent.applyStyle(styledChild, declmap, window)
+      map = styledChild.calcRules(ua, user, author, window)
+      styledChild.applyDeclarations(rootProperties(), map, window)
   return styledChild
 
 proc stackAppend(styledStack: var seq[CascadeFrame]; frame: CascadeFrame;
@@ -461,8 +431,8 @@ proc stackAppend(styledStack: var seq[CascadeFrame]; frame: CascadeFrame;
   ))
 
 proc stackAppend(styledStack: var seq[CascadeFrame]; frame: CascadeFrame;
-    styledParent: StyledNode; pseudo: PseudoElem; i: var int;
-    parentDeclMap: RuleListMap = nil) =
+    styledParent: StyledNode; pseudo: PseudoElement; i: var int;
+    parentMap: RuleListMap = nil) =
   # Can't check for cachedChildren.len here, because we assume that we only have
   # cached pseudo elems when the parent is also cached.
   if frame.cachedChild != nil:
@@ -483,25 +453,25 @@ proc stackAppend(styledStack: var seq[CascadeFrame]; frame: CascadeFrame;
         styledParent: styledParent,
         pseudo: pseudo,
         cachedChild: cached,
-        parentDeclMap: parentDeclMap
+        parentMap: parentMap
       ))
   else:
     styledStack.add(CascadeFrame(
       styledParent: styledParent,
       pseudo: pseudo,
       cachedChild: nil,
-      parentDeclMap: parentDeclMap
+      parentMap: parentMap
     ))
 
 # Append children to styledChild.
 proc appendChildren(styledStack: var seq[CascadeFrame]; frame: CascadeFrame;
-    styledChild: StyledNode; parentDeclMap: RuleListMap) =
+    styledChild: StyledNode; parentMap: RuleListMap) =
   # i points to the child currently being inspected.
   var idx = frame.cachedChildren.len - 1
   let element = Element(styledChild.node)
   # reset invalid flag here to avoid a type conversion above
   element.invalid = false
-  styledStack.stackAppend(frame, styledChild, peAfter, idx, parentDeclMap)
+  styledStack.stackAppend(frame, styledChild, peAfter, idx, parentMap)
   case element.tagType
   of TAG_TEXTAREA:
     styledStack.stackAppend(frame, styledChild, peTextareaText, idx)
@@ -520,7 +490,7 @@ proc appendChildren(styledStack: var seq[CascadeFrame]; frame: CascadeFrame;
         styledStack.stackAppend(frame, styledChild, child, idx)
     if element.tagType == TAG_INPUT:
       styledStack.stackAppend(frame, styledChild, peInputText, idx)
-  styledStack.stackAppend(frame, styledChild, peBefore, idx, parentDeclMap)
+  styledStack.stackAppend(frame, styledChild, peBefore, idx, parentMap)
 
 # Builds a StyledNode tree, optionally based on a previously cached version.
 proc applyRules(document: Document; ua, user: CSSStylesheet;
@@ -538,7 +508,7 @@ proc applyRules(document: Document; ua, user: CSSStylesheet;
   var toReset: seq[Element] = @[]
   while styledStack.len > 0:
     var frame = styledStack.pop()
-    var declmap: RuleListMap
+    var map: RuleListMap = nil
     let styledParent = frame.styledParent
     let valid = frame.cachedChild != nil and frame.cachedChild.isValid(toReset)
     let styledChild = if valid:
@@ -547,15 +517,14 @@ proc applyRules(document: Document; ua, user: CSSStylesheet;
       # From here on, computed values of this node's children are invalid
       # because of property inheritance.
       frame.cachedChild = nil
-      frame.applyRulesFrameInvalid(ua, user, author, declmap,
-        document.window)
+      frame.applyRulesFrameInvalid(ua, user, author, map, document.window)
     if styledChild != nil:
       if styledParent == nil:
         # Root element
         root = styledChild
       if styledChild.t == stElement and styledChild.node != nil:
         # note: following resets styledChild.node's invalid flag
-        styledStack.appendChildren(frame, styledChild, declmap)
+        styledStack.appendChildren(frame, styledChild, map)
   for element in toReset:
     element.invalidDeps = {}
   return root
diff --git a/src/css/cssvalues.nim b/src/css/cssvalues.nim
index fa24b39b..a2c661d6 100644
--- a/src/css/cssvalues.nim
+++ b/src/css/cssvalues.nim
@@ -7,14 +7,13 @@ import std/tables
 import css/cssparser
 import css/lunit
 import css/selectorparser
+import html/catom
 import types/bitmap
 import types/color
 import types/opt
 import types/winattrs
 import utils/twtstr
 
-export selectorparser.PseudoElem
-
 type
   CSSPropertyType* = enum
     # primitive/enum properties: stored as byte
@@ -410,13 +409,26 @@ type
     coUser
     coAuthor
 
+  CSSEntryType* = enum
+    ceBit, ceObject, ceWord, ceVar
+
   CSSComputedEntry* = object
     t*: CSSPropertyType
     global*: CSSGlobalType
-    #TODO case on t?
-    bit*: uint8
-    word*: CSSValueWord
-    obj*: CSSValue
+    case et*: CSSEntryType
+    of ceBit:
+      bit*: uint8
+    of ceWord:
+      word*: CSSValueWord
+    of ceObject:
+      obj*: CSSValue
+    of ceVar:
+      cvar*: CAtom
+
+  CSSVariable* = ref object
+    name*: CAtom
+    cvals*: seq[CSSComponentValue]
+    resolved: seq[tuple[v: CSSValueType; entry: CSSComputedEntry]]
 
 static:
   doAssert sizeof(CSSValueBit) == 1
@@ -1247,34 +1259,66 @@ func cssNumber(cval: CSSComponentValue; positive: bool): Opt[float32] =
 
 proc makeEntry*(t: CSSPropertyType; obj: CSSValue; global = cgtNone):
     CSSComputedEntry =
-  return CSSComputedEntry(t: t, obj: obj, global: global)
+  return CSSComputedEntry(et: ceObject, t: t, obj: obj, global: global)
 
 proc makeEntry*(t: CSSPropertyType; word: CSSValueWord; global = cgtNone):
     CSSComputedEntry =
-  return CSSComputedEntry(t: t, word: word, global: global)
+  return CSSComputedEntry(et: ceWord, t: t, word: word, global: global)
 
 proc makeEntry*(t: CSSPropertyType; bit: CSSValueBit; global = cgtNone):
     CSSComputedEntry =
-  return CSSComputedEntry(t: t, bit: bit.dummy, global: global)
+  return CSSComputedEntry(et: ceBit, t: t, bit: bit.dummy, global: global)
 
 proc makeEntry*(t: CSSPropertyType; global: CSSGlobalType): CSSComputedEntry =
-  return CSSComputedEntry(t: t, global: global)
+  return CSSComputedEntry(et: ceObject, t: t, global: global)
+
+proc parseVar(fun: CSSFunction; entry: var CSSComputedEntry;
+    attrs: WindowAttributes; factory: CAtomFactory): Opt[void] =
+  let i = fun.value.skipBlanks(0)
+  if i >= fun.value.len or fun.value.skipBlanks(i + 1) < fun.value.len:
+    return err()
+  let cval = fun.value[i]
+  if not (cval of CSSToken):
+    return err()
+  let tok = CSSToken(fun.value[i])
+  if tok.t != cttIdent:
+    return err()
+  entry = CSSComputedEntry(
+    et: ceVar,
+    cvar: factory.toAtom(tok.value.substr(2))
+  )
+  return ok()
 
 proc parseValue(cvals: openArray[CSSComponentValue];
-    entry: var CSSComputedEntry; attrs: WindowAttributes): Opt[void] =
+    entry: var CSSComputedEntry; attrs: WindowAttributes;
+    factory: CAtomFactory): Opt[void] =
   var i = cvals.skipBlanks(0)
   if i >= cvals.len:
     return err()
   let cval = cvals[i]
-  let t = entry.t
   inc i
+  if cval of CSSFunction:
+    let fun = CSSFunction(cval)
+    if fun.name.equalsIgnoreCase("var"):
+      if cvals.skipBlanks(i) < cvals.len:
+        return err()
+      return fun.parseVar(entry, attrs, factory)
+  let t = entry.t
   let v = valueType(t)
   template set_new(prop, val: untyped) =
-    entry.obj = CSSValue(v: v, prop: val)
+    entry = CSSComputedEntry(
+      t: entry.t,
+      et: ceObject,
+      obj: CSSValue(v: v, prop: val)
+    )
   template set_word(prop, val: untyped) =
-    entry.word = CSSValueWord(prop: val)
+    entry = CSSComputedEntry(
+      t: entry.t,
+      et: ceWord,
+      word: CSSValueWord(prop: val)
+    )
   template set_bit(prop, val: untyped) =
-    entry.bit = cast[uint8](val)
+    entry = CSSComputedEntry(t: entry.t, et: ceBit, bit: cast[uint8](val))
   case v
   of cvtDisplay: set_bit display, ?parseIdent[CSSDisplay](cval)
   of cvtWhiteSpace: set_bit whiteSpace, ?parseIdent[CSSWhiteSpace](cval)
@@ -1426,7 +1470,8 @@ const PropertyPaddingSpec = [
 ]
 
 proc parseComputedValues*(res: var seq[CSSComputedEntry]; name: string;
-    cvals: openArray[CSSComponentValue]; attrs: WindowAttributes): Err[void] =
+    cvals: openArray[CSSComponentValue]; attrs: WindowAttributes;
+    factory: CAtomFactory): Err[void] =
   var i = cvals.skipBlanks(0)
   if i >= cvals.len:
     return err()
@@ -1439,8 +1484,12 @@ proc parseComputedValues*(res: var seq[CSSComputedEntry]; name: string;
       if global != cgtNone:
         res.add(makeEntry(t, global))
       else:
-        var entry = CSSComputedEntry(t: t)
-        ?cvals.parseValue(entry, attrs)
+        let et = case t.reprType
+        of cprtBit: ceBit
+        of cprtObject: ceObject
+        of cprtWord: ceWord
+        var entry = CSSComputedEntry(t: t, et: et)
+        ?cvals.parseValue(entry, attrs, factory)
         res.add(entry)
   of cstAll:
     if global == cgtNone:
@@ -1561,9 +1610,9 @@ proc parseComputedValues*(res: var seq[CSSComputedEntry]; name: string;
   return ok()
 
 proc parseComputedValues*(name: string; value: seq[CSSComponentValue];
-    attrs: WindowAttributes): seq[CSSComputedEntry] =
+    attrs: WindowAttributes; factory: CAtomFactory): seq[CSSComputedEntry] =
   var res: seq[CSSComputedEntry] = @[]
-  if res.parseComputedValues(name, value, attrs).isSome:
+  if res.parseComputedValues(name, value, attrs, factory).isSome:
     return res
   return @[]
 
@@ -1591,8 +1640,13 @@ type
 
   InitMap* = array[CSSPropertyType, set[InitType]]
 
+  CSSVariableMap* = ref object
+    parent*: CSSVariableMap
+    vars*: seq[CSSVariable]
+
 proc applyValue*(vals: CSSValues; entry: CSSComputedEntry;
-    parent, previousOrigin: CSSValues; inited: InitMap; initType: InitType) =
+    varMap: CSSVariableMap; parent, previousOrigin: CSSValues; inited: InitMap;
+    initType: InitType) =
   case entry.global
   of cgtInherit:
     if parent != nil:
@@ -1609,10 +1663,11 @@ proc applyValue*(vals: CSSValues; entry: CSSComputedEntry;
     else:
       vals.initialOrInheritFrom(parent, entry.t)
   of cgtNone:
-    case entry.t.reprType
-    of cprtBit: vals.bits[entry.t].dummy = entry.bit
-    of cprtWord: vals.words[entry.t] = entry.word
-    of cprtObject: vals.objs[entry.t] = entry.obj
+    case entry.et
+    of ceBit: vals.bits[entry.t].dummy = entry.bit
+    of ceWord: vals.words[entry.t] = entry.word
+    of ceObject: vals.objs[entry.t] = entry.obj
+    of ceVar: discard #eprint "TODO"
 
 func inheritProperties*(parent: CSSValues): CSSValues =
   result = CSSValues()
diff --git a/src/css/selectorparser.nim b/src/css/selectorparser.nim
index c66a1b43..a927b9be 100644
--- a/src/css/selectorparser.nim
+++ b/src/css/selectorparser.nim
@@ -9,7 +9,7 @@ type
   SelectorType* = enum
     stType, stId, stAttr, stClass, stUniversal, stPseudoClass, stPseudoElement
 
-  PseudoElem* = enum
+  PseudoElement* = enum
     peNone = "-cha-none"
     peBefore = "before"
     peAfter = "after"
@@ -91,7 +91,7 @@ type
     of stPseudoClass:
       pseudo*: PseudoData
     of stPseudoElement:
-      elem*: PseudoElem
+      elem*: PseudoElement
 
   PseudoData* = object
     case t*: PseudoClass
@@ -108,10 +108,19 @@ type
     ct*: CombinatorType # relation to the next entry in a ComplexSelector.
     sels*: seq[Selector]
 
-  ComplexSelector* = seq[CompoundSelector]
+  ComplexSelector* = object
+    specificity*: int
+    pseudo*: PseudoElement
+    csels: seq[CompoundSelector]
 
   SelectorList* = seq[ComplexSelector]
 
+# Forward declarations
+proc parseSelectorList(cvals: seq[CSSComponentValue]; factory: CAtomFactory;
+  nested, forgiving: bool): SelectorList
+proc parseComplexSelector(state: var SelectorParser): ComplexSelector
+func `$`*(cxsel: ComplexSelector): string
+
 iterator items*(sels: CompoundSelector): Selector {.inline.} =
   for it in sels.sels:
     yield it
@@ -128,7 +137,26 @@ func len*(sels: CompoundSelector): int {.inline.} =
 proc add*(sels: var CompoundSelector; sel: Selector) {.inline.} =
   sels.sels.add(sel)
 
-func `$`*(cxsel: ComplexSelector): string
+func `[]`*(cxsel: ComplexSelector; i: int): lent CompoundSelector {.inline.} =
+  return cxsel.csels[i]
+
+func `[]`*(cxsel: ComplexSelector; i: BackwardsIndex): CompoundSelector
+    {.inline.} =
+  return cxsel.csels[i]
+
+func `[]`*(cxsel: var ComplexSelector; i: BackwardsIndex): var CompoundSelector
+    {.inline.} =
+  return cxsel.csels[i]
+
+func len*(cxsel: ComplexSelector): int {.inline.} =
+  return cxsel.csels.len
+
+func high*(cxsel: ComplexSelector): int {.inline.} =
+  return cxsel.csels.high
+
+iterator items*(cxsel: ComplexSelector): lent CompoundSelector {.inline.} =
+  for it in cxsel.csels:
+    yield it
 
 func `$`*(sel: Selector): string =
   case sel.t
@@ -212,8 +240,6 @@ func `$`*(slist: SelectorList): string =
     result &= $cxsel
     s = true
 
-func getSpecificity*(cxsel: ComplexSelector): int
-
 func getSpecificity(sel: Selector): int =
   case sel.t
   of stId:
@@ -225,7 +251,7 @@ func getSpecificity(sel: Selector): int =
     of pcIs, pcNot:
       var best = 0
       for child in sel.pseudo.fsels:
-        let s = getSpecificity(child)
+        let s = child.specificity
         if s > best:
           best = s
       result += best
@@ -233,7 +259,7 @@ func getSpecificity(sel: Selector): int =
       if sel.pseudo.ofsels.len != 0:
         var best = 0
         for child in sel.pseudo.ofsels:
-          let s = getSpecificity(child)
+          let s = child.specificity
           if s > best:
             best = s
         result += best
@@ -245,19 +271,11 @@ func getSpecificity(sel: Selector): int =
   of stUniversal:
     discard
 
-func getSpecificity*(sels: CompoundSelector): int =
+func getSpecificity(sels: CompoundSelector): int =
+  result= 0
   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
@@ -277,9 +295,6 @@ template get_tok(cval: CSSComponentValue): CSSToken =
   if not (c of CSSToken): fail
   CSSToken(c)
 
-proc parseSelectorList(cvals: seq[CSSComponentValue]; factory: CAtomFactory;
-  nested, forgiving: bool): SelectorList
-
 # Functions that may contain other selectors, functions, etc.
 proc parseRecursiveSelectorFunction(state: var SelectorParser;
     class: PseudoClass; body: seq[CSSComponentValue]; forgiving: bool):
@@ -353,7 +368,7 @@ 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) =
+    template add_pseudo_element(element: PseudoElement) =
       state.skipWhitespace()
       if state.nested or state.has() and state.peek() != cttComma: fail
       return Selector(t: stPseudoElement, elem: element)
@@ -391,8 +406,6 @@ proc parsePseudoSelector(state: var SelectorParser): Selector =
     return state.parseSelectorFunction(CSSFunction(cval))
   else: fail
 
-proc parseComplexSelector(state: var SelectorParser): ComplexSelector
-
 proc parseAttributeSelector(state: var SelectorParser;
     cssblock: CSSSimpleBlock): Selector =
   if cssblock.token.t != cttLbracket: fail
@@ -438,10 +451,7 @@ proc parseAttributeSelector(state: var SelectorParser;
     t: stAttr,
     attr: state.factory.toAtom(attr.value),
     value: value.value,
-    rel: SelectorRelation(
-      t: rel,
-      flag: flag
-    )
+    rel: SelectorRelation(t: rel, flag: flag)
   )
 
 proc parseClassSelector(state: var SelectorParser): Selector =
@@ -504,10 +514,15 @@ proc parseCompoundSelector(state: var SelectorParser): CompoundSelector =
       fail
 
 proc parseComplexSelector(state: var SelectorParser): ComplexSelector =
+  result = ComplexSelector()
   while true:
     state.skipWhitespace()
     let sels = state.parseCompoundSelector()
-    result.add(sels)
+    if state.failed:
+      break
+    #TODO propagate from parser
+    result.specificity += sels.getSpecificity()
+    result.csels.add(sels)
     if sels.len == 0: fail
     if not state.has():
       break # finish
@@ -526,6 +541,9 @@ proc parseComplexSelector(state: var SelectorParser): ComplexSelector =
     else: fail
   if result.len == 0 or result[^1].ct != ctNone:
     fail
+  if result[^1][^1].t == stPseudoElement:
+    #TODO move pseudo check here?
+    result.pseudo = result[^1][^1].elem
 
 proc parseSelectorList(cvals: seq[CSSComponentValue]; factory: CAtomFactory;
     nested, forgiving: bool): SelectorList =
diff --git a/src/css/sheet.nim b/src/css/sheet.nim
index ee92198c..7d1359f9 100644
--- a/src/css/sheet.nim
+++ b/src/css/sheet.nim
@@ -1,4 +1,5 @@
 import std/options
+import std/strutils
 import std/tables
 
 import css/cssparser
@@ -6,6 +7,7 @@ import css/cssvalues
 import css/mediaquery
 import css/selectorparser
 import html/catom
+import types/opt
 import types/url
 import types/winattrs
 import utils/twtstr
@@ -15,11 +17,14 @@ type
 
   CSSRuleDef* = ref object of CSSRuleBase
     sels*: SelectorList
+    specificity*: int
     normalVals*: seq[CSSComputedEntry]
     importantVals*: seq[CSSComputedEntry]
+    normalVars*: seq[CSSVariable]
+    importantVars*: seq[CSSVariable]
     # Absolute position in the stylesheet; used for sorting rules after
     # retrieval from the cache.
-    idx: int
+    idx*: int
 
   CSSMediaQueryDef* = ref object of CSSRuleBase
     children*: CSSStylesheet
@@ -130,9 +135,6 @@ proc getSelectorIds(hashes: var SelectorHashes; sel: Selector): bool =
     return hashes.tag != CAtomNull or hashes.id != CAtomNull or
       hashes.class != CAtomNull
 
-proc ruleDefCmp*(a, b: CSSRuleDef): int =
-  cmp(a.idx, b.idx)
-
 proc add(sheet: CSSStylesheet; rule: CSSRuleDef) =
   var hashes = SelectorHashes()
   for cxsel in rule.sels:
@@ -179,23 +181,35 @@ proc add*(sheet, sheet2: CSSStylesheet) =
       sheet.attrTable[key] = value
 
 proc addRule(sheet: CSSStylesheet; rule: CSSQualifiedRule) =
-  let sels = parseSelectors(rule.prelude, sheet.factory)
+  var sels = parseSelectors(rule.prelude, sheet.factory)
   if sels.len > 0:
-    var normalVals: seq[CSSComputedEntry] = @[]
-    var importantVals: seq[CSSComputedEntry] = @[]
     let decls = rule.oblock.value.parseDeclarations()
+    var pass2 = newSeqOfCap[CSSDeclaration](decls.len)
+    let rule = CSSRuleDef(sels: move(sels), idx: sheet.len)
     for decl in decls:
-      let vals = parseComputedValues(decl.name, decl.value, sheet.attrs[])
+      if decl.name.startsWith("--"):
+        let cvar = CSSVariable(
+          # case sensitive, it seems
+          name: sheet.factory.toAtom(decl.name.substr(2)),
+          cvals: move(decl.value)
+        )
+        if decl.important:
+          rule.importantVars.add(cvar)
+        else:
+          rule.normalVars.add(cvar)
+      pass2.add(decl)
+    for decl in pass2:
       if decl.important:
-        importantVals.add(vals)
+        let olen = rule.importantVals.len
+        if rule.importantVals.parseComputedValues(decl.name, decl.value,
+            sheet.attrs[], sheet.factory).isNone:
+          rule.importantVals.setLen(olen)
       else:
-        normalVals.add(vals)
-    sheet.add(CSSRuleDef(
-      sels: sels,
-      normalVals: normalVals,
-      importantVals: importantVals,
-      idx: sheet.len
-    ))
+        let olen = rule.normalVals.len
+        if rule.normalVals.parseComputedValues(decl.name, decl.value,
+            sheet.attrs[], sheet.factory).isNone:
+          rule.normalVals.setLen(olen)
+    sheet.add(rule)
     inc sheet.len
 
 proc addAtRule(sheet: CSSStylesheet; atrule: CSSAtRule; base: URL) =
diff --git a/src/css/stylednode.nim b/src/css/stylednode.nim
index 46df5ff0..c1a4cbc8 100644
--- a/src/css/stylednode.nim
+++ b/src/css/stylednode.nim
@@ -47,7 +47,7 @@ type
   StyledNode* = ref object
     parent*: StyledNode
     node*: Node
-    pseudo*: PseudoElem
+    pseudo*: PseudoElement
     case t*: StyledType
     of stText:
       discard
@@ -108,11 +108,9 @@ func newStyledElement*(parent: StyledNode; element: Element): StyledNode =
 func newStyledElement*(element: Element): StyledNode =
   return StyledNode(t: stElement, node: element)
 
-func newStyledElement*(parent: StyledNode; pseudo: PseudoElem;
-    computed: CSSValues): StyledNode =
+func newStyledElement*(parent: StyledNode; pseudo: PseudoElement): StyledNode =
   return StyledNode(
     t: stElement,
-    computed: computed,
     pseudo: pseudo,
     parent: parent
   )
@@ -124,7 +122,7 @@ func newStyledText*(text: string): StyledNode =
   return StyledNode(t: stText, node: CharacterData(data: text))
 
 func newStyledReplacement*(parent: StyledNode; content: CSSContent;
-    pseudo: PseudoElem): StyledNode =
+    pseudo: PseudoElement): StyledNode =
   return StyledNode(
     t: stReplacement,
     parent: parent,
diff --git a/src/html/dom.nim b/src/html/dom.nim
index 49233e87..83ad9025 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -3796,8 +3796,9 @@ proc setValue(this: CSSStyleDeclaration; i: int; cvals: seq[CSSComponentValue]):
   if i notin 0 .. this.decls.high:
     return err()
   # dummyAttrs can be safely used because the result is discarded.
-  var dummy: seq[CSSComputedEntry]
-  ?parseComputedValues(dummy, this.decls[i].name, cvals, dummyAttrs)
+  var dummy: seq[CSSComputedEntry] = @[]
+  ?dummy.parseComputedValues(this.decls[i].name, cvals, dummyAttrs,
+    this.element.document.factory)
   this.decls[i].value = cvals
   return ok()
 
@@ -3831,8 +3832,9 @@ proc setProperty(this: CSSStyleDeclaration; name, value: string):
       # not err! this does not throw.
       return ok()
   else:
-    var dummy: seq[CSSComputedEntry]
-    let val0 = parseComputedValues(dummy, name, cvals, dummyAttrs)
+    var dummy: seq[CSSComputedEntry] = @[]
+    let val0 = dummy.parseComputedValues(name, cvals, dummyAttrs,
+      this.element.document.factory)
     if val0.isNone:
       return ok()
     this.decls.add(CSSDeclaration(name: name, value: cvals))