about summary refs log tree commit diff stats
path: root/src/css
diff options
context:
space:
mode:
Diffstat (limited to 'src/css')
-rw-r--r--src/css/cascade.nim84
-rw-r--r--src/css/select.nim223
-rw-r--r--src/css/stylednode.nim115
-rw-r--r--src/css/values.nim2
4 files changed, 284 insertions, 140 deletions
diff --git a/src/css/cascade.nim b/src/css/cascade.nim
index 8b93e8d0..25866ac0 100644
--- a/src/css/cascade.nim
+++ b/src/css/cascade.nim
@@ -21,9 +21,6 @@ type
 proc applyProperty(styledNode: StyledNode, parent: CSSComputedValues, d: CSSDeclaration) =
   styledNode.computed.applyValue(parent, d)
 
-  if styledNode.node != nil:
-    Element(styledNode.node).cssapplied = true
-
 func applies(mq: MediaQuery): bool =
   case mq.t
   of CONDITION_MEDIA:
@@ -59,16 +56,17 @@ func applies(mqlist: MediaQueryList): bool =
 
 type ToSorts = array[PseudoElem, seq[(int, seq[CSSDeclaration])]]
 
-proc calcRule(tosorts: var ToSorts, elem: Element, rule: CSSRuleDef) =
+proc calcRule(tosorts: var ToSorts, styledNode: StyledNode, rule: CSSRuleDef) =
   for sel in rule.sels:
-    if elem.selectorsMatch(sel):
+    if styledNode.selectorsMatch(sel):
       let spec = getSpecificity(sel)
       tosorts[sel.pseudo].add((spec,rule.decls))
 
-func calcRules(elem: Element, sheet: CSSStylesheet): DeclarationList =
+func calcRules(styledNode: StyledNode, sheet: CSSStylesheet): DeclarationList =
   var tosorts: ToSorts
+  let elem = Element(styledNode.node)
   for rule in sheet.gen_rules(elem.tagType, elem.id, elem.classList):
-    tosorts.calcRule(elem, rule)
+    tosorts.calcRule(styledNode, rule)
 
   for i in PseudoElem:
     tosorts[i].sort((x, y) => cmp(x[0], y[0]))
@@ -88,8 +86,7 @@ proc applyImportant(ares: var ApplyResult, decls: seq[CSSDeclaration]) =
     if decl.important:
       ares.important.add(decl)
 
-# Always returns a new styled node, with the passed declarations applied.
-proc applyDeclarations(elem: Element, parent: CSSComputedValues, ua, user: DeclarationList, author: seq[DeclarationList]): StyledNode =
+proc applyDeclarations(styledNode: StyledNode, parent: CSSComputedValues, ua, user: DeclarationList, author: seq[DeclarationList]) =
   let pseudo = PSEUDO_NONE
   var ares: ApplyResult
 
@@ -101,21 +98,22 @@ proc applyDeclarations(elem: Element, parent: CSSComputedValues, ua, user: Decla
   for rule in author:
     ares.applyImportant(rule[pseudo])
 
-  let style = elem.attr("style")
-  if style.len > 0:
-    let inline_rules = newStringStream(style).parseListOfDeclarations2()
-    ares.applyNormal(inline_rules)
-    ares.applyImportant(inline_rules)
+  if styledNode.node != nil:
+    let style = Element(styledNode.node).attr("style")
+    if style.len > 0:
+      let inline_rules = newStringStream(style).parseListOfDeclarations2()
+      ares.applyNormal(inline_rules)
+      ares.applyImportant(inline_rules)
 
   ares.applyImportant(user[pseudo])
   ares.applyImportant(ua[pseudo])
 
-  result = StyledNode(t: STYLED_ELEMENT, node: elem, computed: parent.inheritProperties())
+  styledNode.computed = parent.inheritProperties()
   for rule in ares.normal:
-    result.applyProperty(parent, rule)
+    styledNode.applyProperty(parent, rule)
 
   for rule in ares.important:
-    result.applyProperty(parent, rule)
+    styledNode.applyProperty(parent, rule)
 
 # Either returns a new styled node or nil.
 proc applyDeclarations(pseudo: PseudoElem, parent: CSSComputedValues, ua, user: DeclarationList, author: seq[DeclarationList]): StyledNode =
@@ -146,23 +144,21 @@ func applyMediaQuery(ss: CSSStylesheet): CSSStylesheet =
     if mq.query.applies():
       result.add(mq.children.applyMediaQuery())
 
-func calcRules(elem: Element, ua, user: CSSStylesheet, author: seq[CSSStylesheet]): tuple[uadecls, userdecls: DeclarationList, authordecls: seq[DeclarationList]] =
-  result.uadecls = calcRules(elem, ua)
-  result.userdecls = calcRules(elem, user)
+func calcRules(styledNode: StyledNode, ua, user: CSSStylesheet, author: seq[CSSStylesheet]): tuple[uadecls, userdecls: DeclarationList, authordecls: seq[DeclarationList]] =
+  result.uadecls = calcRules(styledNode, ua)
+  result.userdecls = calcRules(styledNode, user)
   for rule in author:
-    result.authordecls.add(calcRules(elem, rule))
+    result.authordecls.add(calcRules(styledNode, rule))
 
-proc applyStyle(parent: StyledNode, elem: Element, uadecls, userdecls: DeclarationList, authordecls: seq[DeclarationList]): StyledNode =
+proc applyStyle(parent, styledNode: StyledNode, uadecls, userdecls: DeclarationList, authordecls: seq[DeclarationList]) =
   let parentComputed = if parent != nil:
     parent.computed
   else:
     rootProperties()
 
-  result = elem.applyDeclarations(parentComputed, uadecls, userdecls, authordecls)
-  assert result != nil
+  styledNode.applyDeclarations(parentComputed, uadecls, userdecls, authordecls)
 
 # Builds a StyledNode tree, optionally based on a previously cached version.
-# This was originally a recursive algorithm; it had to be rewritten iteratively
 proc applyRules(document: Document, ua, user: CSSStylesheet, cachedTree: StyledNode): StyledNode =
   if document.html == nil:
     return
@@ -187,14 +183,17 @@ proc applyRules(document: Document, ua, user: CSSStylesheet, cachedTree: StyledN
       continue
 
     var styledChild: StyledNode
-    if cachedChild != nil and (cachedChild.node == nil or cachedChild.node.nodeType != ELEMENT_NODE or Element(cachedChild.node).cssapplied):
+    if cachedChild != nil and cachedChild.isValid():
       if cachedChild.t == STYLED_ELEMENT:
-        styledChild = StyledNode(t: STYLED_ELEMENT, pseudo: cachedChild.pseudo, computed: cachedChild.computed, node: cachedChild.node)
-        if cachedChild.pseudo != PSEUDO_NONE:
+        if cachedChild.pseudo == PSEUDO_NONE:
+          styledChild = styledParent.newStyledElement(Element(cachedChild.node), cachedChild.computed, cachedChild.depends)
+        else:
+          styledChild = styledParent.newStyledElement(cachedChild.pseudo, cachedChild.computed, cachedChild.depends)
           styledChild.children = cachedChild.children #TODO does this actually refresh pseudo elems when needed?
       else:
         # Text
-        styledChild = StyledNode(t: STYLED_TEXT, text: cachedChild.text, node: cachedChild.node)
+        styledChild = styledParent.newStyledText(cachedChild.text)
+        styledChild.node = cachedChild.node
       if styledParent == nil:
         # Root element
         result = styledChild
@@ -202,7 +201,7 @@ proc applyRules(document: Document, ua, user: CSSStylesheet, cachedTree: StyledN
         styledParent.children.add(styledChild)
     else:
       if pseudo != PSEUDO_NONE:
-        let (ua, user, authordecls) = Element(styledParent.node).calcRules(ua, user, author)
+        let (ua, user, authordecls) = styledParent.calcRules(ua, user, author)
         case pseudo
         of PSEUDO_BEFORE, PSEUDO_AFTER:
           let styledPseudo = pseudo.applyDeclarations(styledParent.computed, ua, user, authordecls)
@@ -210,32 +209,34 @@ proc applyRules(document: Document, ua, user: CSSStylesheet, cachedTree: StyledN
             styledParent.children.add(styledPseudo)
             let content = styledPseudo.computed{"content"}
             if content.len > 0:
-              styledPseudo.children.add(StyledNode(t: STYLED_TEXT, text: content))
+              styledPseudo.children.add(styledPseudo.newStyledText(content))
         of PSEUDO_INPUT_TEXT:
           let content = HTMLInputElement(styledParent.node).inputString()
           if content.len > 0:
-            styledChild = StyledNode(t: STYLED_TEXT, text: content)
+            styledChild = styledParent.newStyledText(content)
             styledParent.children.add(styledChild)
         of PSEUDO_NONE: discard
       else:
         assert child != nil
         if styledParent != nil:
           if child.nodeType == ELEMENT_NODE:
-            let (ua, user, authordecls) = Element(child).calcRules(ua, user, author)
-            styledChild = applyStyle(styledParent, Element(child), ua, user, authordecls)
+            styledChild = styledParent.newStyledElement(Element(child))
+            let (ua, user, authordecls) = styledChild.calcRules(ua, user, author)
+            applyStyle(styledParent, styledChild, ua, user, authordecls)
             styledParent.children.add(styledChild)
           elif child.nodeType == TEXT_NODE:
             let text = Text(child)
-            styledChild = StyledNode(t: STYLED_TEXT, node: child, text: text.data)
+            styledChild = styledParent.newStyledText(text)
             styledParent.children.add(styledChild)
         else:
           # Root element
-          assert result == nil
-          let (ua, user, authordecls) = Element(child).calcRules(ua, user, author)
-          styledChild = applyStyle(styledParent, Element(child), ua, user, authordecls)
+          styledChild = newStyledElement(Element(child))
+          let (ua, user, authordecls) = styledChild.calcRules(ua, user, author)
+          applyStyle(styledParent, styledChild, ua, user, authordecls)
           result = styledChild
 
-    if styledChild != nil and styledChild.node != nil and styledChild.node.nodeType == ELEMENT_NODE:
+    if styledChild != nil and styledChild.t == STYLED_ELEMENT and styledChild.node != nil:
+      styledChild.applyDependValues()
       template stack_append(styledParent: StyledNode, child: Node) =
         if cachedChild != nil:
           var cached: StyledNode
@@ -280,8 +281,3 @@ proc applyStylesheets*(document: Document, uass, userss: CSSStylesheet, previous
   let uass = uass.applyMediaQuery()
   let userss = userss.applyMediaQuery()
   return document.applyRules(uass, userss, previousStyled)
-
-proc refreshStyle*(elem: Element) =
-  elem.cssapplied = false
-  for child in elem.children:
-    child.refreshStyle()
diff --git a/src/css/select.nim b/src/css/select.nim
index b43604c4..f1140d1e 100644
--- a/src/css/select.nim
+++ b/src/css/select.nim
@@ -1,12 +1,9 @@
-import unicode
 import tables
 import strutils
-import sequtils
-import sugar
-import streams
 
-import css/selectorparser
 import css/cssparser
+import css/selectorparser
+import css/stylednode
 import html/dom
 import html/tags
 
@@ -14,7 +11,7 @@ func attrSelectorMatches(elem: Element, sel: Selector): bool =
   case sel.rel
   of ' ': return sel.attr in elem.attributes
   of '=': return elem.attr(sel.attr) == sel.value
-  of '~': return sel.value in unicode.split(elem.attr(sel.attr))
+  of '~': return sel.value in elem.attr(sel.attr).split(Whitespace)
   of '|':
     let val = elem.attr(sel.attr)
     return val == sel.value or sel.value.startsWith(val & '-')
@@ -23,12 +20,17 @@ func attrSelectorMatches(elem: Element, sel: Selector): bool =
   of '*': return elem.attr(sel.attr).contains(sel.value)
   else: return false
 
-func pseudoSelectorMatches(elem: Element, sel: Selector): bool =
+func pseudoSelectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem: T): bool =
+  let selem = elem
+  when elem is StyledNode:
+    let elem = Element(elem.node)
   case sel.pseudo
   of PSEUDO_FIRST_CHILD: return elem.parentNode.firstElementChild == elem
   of PSEUDO_LAST_CHILD: return elem.parentNode.lastElementChild == elem
   of PSEUDO_ONLY_CHILD: return elem.parentNode.firstElementChild == elem and elem.parentNode.lastElementChild == elem
-  of PSEUDO_HOVER: return elem.hover
+  of PSEUDO_HOVER:
+    when selem is StyledNode: felem.depends.nodes[DEPEND_HOVER].add(selem)
+    return elem.hover
   of PSEUDO_ROOT: return elem == elem.document.html
   of PSEUDO_NTH_CHILD:
     let n = int64(sel.pseudonum - 1)
@@ -39,68 +41,101 @@ func pseudoSelectorMatches(elem: Element, sel: Selector): bool =
       inc i
     return false
   of PSEUDO_CHECKED:
+    when selem is StyledNode: felem.depends.nodes[DEPEND_CHECKED].add(selem)
     if elem.tagType == TAG_INPUT:
       return HTMLInputElement(elem).checked
     elif elem.tagType == TAG_OPTION:
       return HTMLOptionElement(elem).selected
     return false
 
-func selectorsMatch*(elem: Element, selectors: SelectorList): bool
+func selectorsMatch*[T: Element|StyledNode](elem: T, selectors: SelectorList, felem: T = nil): bool
 
-func funcSelectorMatches(elem: Element, sel: Selector): bool =
+func funcSelectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem: T): bool =
   case sel.name
   of "not":
     for slist in sel.fsels:
-      if elem.selectorsMatch(slist):
+      if elem.selectorsMatch(slist, felem):
         return false
     return true
   of "is", "where":
     for slist in sel.fsels:
-      if elem.selectorsMatch(slist):
+      if elem.selectorsMatch(slist, felem):
         return true
     return false
   else: discard
 
-func combinatorSelectorMatches(elem: Element, sel: Selector): bool =
+func combinatorSelectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem: T): bool =
+  let selem = elem
   #combinator without at least two members makes no sense
   assert sel.csels.len > 1
-  if elem.selectorsMatch(sel.csels[^1]):
+  if selem.selectorsMatch(sel.csels[^1], felem):
     var i = sel.csels.len - 2
     case sel.ct
     of DESCENDANT_COMBINATOR:
-      var e = elem.parentElement
+      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]):
+        if e.selectorsMatch(sel.csels[i], felem):
           dec i
-        e = e.parentElement
+        when elem is StyledNode:
+          e = e.parent
+        else:
+          e = e.parentElement
     of CHILD_COMBINATOR:
-      var e = elem.parentElement
+      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]):
+        if not e.selectorsMatch(sel.csels[i], felem):
           return false
         dec i
-        e = e.parentElement
+        when elem is StyledNode:
+          e = e.parent
+        else:
+          e = e.parentElement
     of NEXT_SIBLING_COMBINATOR:
       var found = false
-      for child in elem.parentElement.children_rev:
+      when elem is StyledNode:
+        var parent = elem.parent
+      else:
+        var parent = elem.parentElement
+      for child in parent.children_rev:
+        when elem is StyledNode:
+          if child.t != STYLED_ELEMENT or child.node == nil: continue
         if found:
-          if not child.selectorsMatch(sel.csels[i]):
+          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
-      for child in elem.parentElement.children_rev:
+      when selem is StyledNode:
+        var parent = selem.parent
+      else:
+        var parent = elem.parentElement
+      for child in parent.children_rev:
+        when selem is StyledNode:
+          if child.t != STYLED_ELEMENT or child.node == nil: continue
         if found:
-          if child.selectorsMatch(sel.csels[i]):
+          if child.selectorsMatch(sel.csels[i], felem):
             dec i
-        if child == elem:
+          if i < 0:
+            return true
+        if child == selem:
           found = true
     return i == -1
   return false
 
-func selectorMatches(elem: Element, sel: Selector): bool =
+func selectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem: T): bool =
+  let selem = elem
+  when elem is StyledNode:
+    let elem = Element(selem.node)
   case sel.t
   of TYPE_SELECTOR:
     return elem.tagType == sel.tag
@@ -111,78 +146,86 @@ func selectorMatches(elem: Element, sel: Selector): bool =
   of ATTR_SELECTOR:
     return elem.attrSelectorMatches(sel)
   of PSEUDO_SELECTOR:
-    return pseudoSelectorMatches(elem, sel)
+    return pseudoSelectorMatches(selem, sel, felem)
   of PSELEM_SELECTOR:
     return true
   of UNIVERSAL_SELECTOR:
     return true
   of FUNC_SELECTOR:
-    return funcSelectorMatches(elem, sel)
+    return funcSelectorMatches(selem, sel, felem)
   of COMBINATOR_SELECTOR:
-    return combinatorSelectorMatches(elem, sel)
+    return combinatorSelectorMatches(selem, sel, felem)
+
+# 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: SelectorList, felem: T = nil): bool =
+  let felem = if felem != nil:
+    felem
+  else:
+    elem
 
-func selectorsMatch*(elem: Element, selectors: SelectorList): bool =
   for sel in selectors.sels:
-    if not selectorMatches(elem, sel):
+    if not selectorMatches(elem, sel, felem):
       return false
   return true
 
-func selectElems(element: Element, sel: Selector): seq[Element] =
-  case sel.t
-  of TYPE_SELECTOR:
-    return element.filterDescendants((elem) => elem.tagType == sel.tag)
-  of ID_SELECTOR:
-    return element.filterDescendants((elem) => elem.id == sel.id)
-  of CLASS_SELECTOR:
-    return element.filterDescendants((elem) => sel.class in elem.classList)
-  of UNIVERSAL_SELECTOR:
-    return element.all_descendants
-  of ATTR_SELECTOR:
-    return element.filterDescendants((elem) => attrSelectorMatches(elem, sel))
-  of PSEUDO_SELECTOR:
-    return element.filterDescendants((elem) => pseudoSelectorMatches(elem, sel))
-  of PSELEM_SELECTOR:
-    return element.all_descendants
-  of FUNC_SELECTOR:
-    return element.filterDescendants((elem) => selectorMatches(elem, sel))
-  of COMBINATOR_SELECTOR:
-    return element.filterDescendants((elem) => selectorMatches(elem, sel))
-
-func selectElems(element: Element, selectors: SelectorList): seq[Element] =
-  assert(selectors.len > 0)
-  let sellist = optimizeSelectorList(selectors)
-  result = element.selectElems(selectors[0])
-  var i = 1
-
-  while i < sellist.len:
-    result = result.filter((elem) => selectorMatches(elem, sellist[i]))
-    inc i
-
-proc querySelectorAll*(document: Document, q: string): seq[Element] =
-  let ss = newStringStream(q)
-  let cvals = parseListOfComponentValues(ss)
-  let selectors = parseSelectors(cvals)
-
-  if document.html != nil:
-    for sel in selectors:
-      result.add(document.html.selectElems(sel))
-
-proc querySelector*(document: Document, q: string): Element =
-  let elems = document.querySelectorAll(q)
-  if elems.len > 0:
-    return elems[0]
-  return nil
-
-proc querySelectorAll*(element: Element, q: string): seq[Element] =
-  let ss = newStringStream(q)
-  let cvals = parseListOfComponentValues(ss)
-  let selectors = parseSelectors(cvals)
-
-  for sel in selectors:
-    result.add(element.selectElems(sel))
-
-proc querySelector*(element: Element, q: string): Element =
-  let elems = element.querySelectorAll(q)
-  if elems.len > 0:
-    return elems[0]
-  return nil
+#TODO idk, it's not like we have JS anyways
+#func selectElems[T: Element|StyledNode](element: T, sel: Selector, felem: T): seq[T] =
+#  case sel.t
+#  of TYPE_SELECTOR:
+#    return element.filterDescendants((elem) => elem.tagType == sel.tag)
+#  of ID_SELECTOR:
+#    return element.filterDescendants((elem) => elem.id == sel.id)
+#  of CLASS_SELECTOR:
+#    return element.filterDescendants((elem) => sel.class in elem.classList)
+#  of UNIVERSAL_SELECTOR:
+#    return element.all_descendants
+#  of ATTR_SELECTOR:
+#    return element.filterDescendants((elem) => attrSelectorMatches(elem, sel))
+#  of PSEUDO_SELECTOR:
+#    return element.filterDescendants((elem) => pseudoSelectorMatches(elem, sel, felem))
+#  of PSELEM_SELECTOR:
+#    return element.all_descendants
+#  of FUNC_SELECTOR:
+#    return element.filterDescendants((elem) => selectorMatches(elem, sel))
+#  of COMBINATOR_SELECTOR:
+#    return element.filterDescendants((elem) => selectorMatches(elem, sel))
+#
+#func selectElems(element: Element, selectors: SelectorList): seq[Element] =
+#  assert(selectors.len > 0)
+#  let sellist = optimizeSelectorList(selectors)
+#  result = element.selectElems(selectors[0], element)
+#  var i = 1
+#
+#  while i < sellist.len:
+#    result = result.filter((elem) => selectorMatches(elem, sellist[i], elem))
+#    inc i
+#
+#proc querySelectorAll*(document: Document, q: string): seq[Element] =
+#  let ss = newStringStream(q)
+#  let cvals = parseListOfComponentValues(ss)
+#  let selectors = parseSelectors(cvals)
+#
+#  if document.html != nil:
+#    for sel in selectors:
+#      result.add(document.html.selectElems(sel))
+#
+#proc querySelector*(document: Document, q: string): Element =
+#  let elems = document.querySelectorAll(q)
+#  if elems.len > 0:
+#    return elems[0]
+#  return nil
+#
+#proc querySelectorAll*(element: Element, q: string): seq[Element] =
+#  let ss = newStringStream(q)
+#  let cvals = parseListOfComponentValues(ss)
+#  let selectors = parseSelectors(cvals)
+#
+#  for sel in selectors:
+#    result.add(element.selectElems(sel))
+#
+#proc querySelector*(element: Element, q: string): Element =
+#  let elems = element.querySelectorAll(q)
+#  if elems.len > 0:
+#    return elems[0]
+#  return nil
diff --git a/src/css/stylednode.nim b/src/css/stylednode.nim
index d6293187..6c306f21 100644
--- a/src/css/stylednode.nim
+++ b/src/css/stylednode.nim
@@ -1,18 +1,123 @@
+import css/selectorparser
 import css/values
 import html/dom
+import html/tags
 
 # Container to hold a style and a node.
-# Pseudo elements are implemented using StyledNode objects without nodes.
+# Pseudo-elements are implemented using StyledNode objects without nodes. Input
+# elements are implemented as internal "pseudo-elements."
+#
+# To avoid having to invalidate the entire tree on pseudo-class changes, each
+# node holds a list of nodes their CSS values depend on. (This list may include
+# the node itself.) In addition, nodes also store each value valid for
+# dependency d. These are then used for checking the validity of StyledNodes.
+#
+# In other words - say we have to apply the author stylesheets of the following
+# document:
+#
+# <style>
+# div:hover { color: red; }
+# :not(input:checked) + p { display: none; }
+# </style>
+# <div>This div turns red on hover.</div>
+# <input type=checkbox>
+# <p>This paragraph is only shown when the checkbox above is checked.
+#
+# That produces the following dependency graph (simplified):
+# div -> div (hover)
+# p -> input (checked)
+#
+# Then, to check if a node has been invalidated, we just iterate over all
+# recorded dependencies of each StyledNode, and check if their registered value
+# of the pseudo-class still matches that of its associated element.
+#
+# So in our example, for div we check if div's :hover pseudo-class has changed,
+# for p we check whether input's :checked pseudo-class has changed.
+
 type
   StyledType* = enum
     STYLED_ELEMENT, STYLED_TEXT
 
+  DependencyType* = enum
+    DEPEND_HOVER, DEPEND_CHECKED
+
+  InvalidationRegistry* = set[DependencyType]
+
+  DependencyInfo* = object
+    # All nodes we depend on, for each dependency type d.
+    nodes*: array[DependencyType, seq[StyledNode]]
+    # Previous value. Node is marked invalid when one of these no longer
+    # matches the DOM value.
+    prev: array[DependencyType, bool]
+
   StyledNode* = ref object
+    parent*: StyledNode
+    node*: Node
     case t*: StyledType
+    of STYLED_TEXT:
+      text*: string
     of STYLED_ELEMENT:
       pseudo*: PseudoElem
       computed*: CSSComputedValues
-    of STYLED_TEXT:
-      text*: string
-    node*: Node
-    children*: seq[StyledNode]
+      children*: seq[StyledNode]
+      depends*: DependencyInfo
+
+iterator branch*(node: StyledNode): StyledNode {.inline.} =
+  var node = node
+  while node != nil:
+    yield node
+    node = node.parent
+
+iterator children_rev*(node: StyledNode): StyledNode {.inline.} =
+  for i in countdown(node.children.high, 0):
+    yield node.children[i]
+
+func checked(element: Element): bool =
+  if element.tagType == TAG_INPUT:
+    let input = HTMLInputElement(element)
+    result = input.checked
+
+func isValid*(styledNode: StyledNode): bool =
+  if styledNode.t == STYLED_TEXT:
+    return true
+  if styledNode.node != nil and Element(styledNode.node).invalid:
+    return false
+  for d in DependencyType:
+    for child in styledNode.depends.nodes[d]:
+      assert child.node != nil
+      let elem = Element(child.node)
+      case d
+      of DEPEND_HOVER:
+        if child.depends.prev[d] != elem.hover:
+          return false
+      of DEPEND_CHECKED:
+        if child.depends.prev[d] != elem.checked:
+          return false
+  return styledNode.parent == nil or styledNode.parent.isValid()
+
+proc applyDependValues*(styledNode: StyledNode) =
+  let elem = Element(styledNode.node)
+  styledNode.depends.prev[DEPEND_HOVER] = elem.hover
+  styledNode.depends.prev[DEPEND_CHECKED] = elem.checked
+  elem.invalid = false
+
+func newStyledElement*(parent: StyledNode, element: Element, computed: CSSComputedValues, reg: sink DependencyInfo): StyledNode =
+  result = StyledNode(t: STYLED_ELEMENT, computed: computed, node: element, parent: parent)
+  result.depends = reg
+
+func newStyledElement*(parent: StyledNode, element: Element): StyledNode =
+  result = StyledNode(t: STYLED_ELEMENT, node: element, parent: parent)
+
+# Root
+func newStyledElement*(element: Element): StyledNode =
+  result = StyledNode(t: STYLED_ELEMENT, node: element)
+
+func newStyledElement*(parent: StyledNode, pseudo: PseudoElem, computed: CSSComputedValues, reg: sink DependencyInfo): StyledNode =
+  result = StyledNode(t: STYLED_ELEMENT, computed: computed, pseudo: pseudo, parent: parent)
+  result.depends = reg
+
+func newStyledText*(parent: StyledNode, text: string): StyledNode =
+  result = StyledNode(t: STYLED_TEXT, text: text, parent: parent)
+
+func newStyledText*(parent: StyledNode, text: Text): StyledNode =
+  result = StyledNode(t: STYLED_TEXT, text: text.data, node: text, parent: parent)
diff --git a/src/css/values.nim b/src/css/values.nim
index be8f5305..ec172e8d 100644
--- a/src/css/values.nim
+++ b/src/css/values.nim
@@ -589,7 +589,7 @@ func cssDisplay(d: CSSDeclaration): CSSDisplay =
       of "inline": return DISPLAY_INLINE
       of "list-item": return DISPLAY_LIST_ITEM
       of "inline-block": return DISPLAY_INLINE_BLOCK
-      of "table": return DISPLAY_TABLE
+      #of "table": return DISPLAY_TABLE
       # of "table-row": return DISPLAY_TABLE_ROW
       # of "table-cell": return DISPLAY_TABLE_CELL
       # of "table-column": return DISPLAY_TABLE_COLUMN