about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2022-07-27 22:28:14 +0200
committerbptato <nincsnevem662@gmail.com>2022-07-27 23:26:24 +0200
commitdd9cb39c7d6adfa02feb181dd629bb9a93b7250c (patch)
tree4d4f840847a20577729f12a879c95c7a29a859b7
parentcc92092fe8053f77d0c63e57c3990272b36be4ef (diff)
downloadchawan-dd9cb39c7d6adfa02feb181dd629bb9a93b7250c.tar.gz
Fix StyledNode invalidation & others
-rw-r--r--readme.md10
-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
-rw-r--r--src/html/dom.nim7
-rw-r--r--src/io/buffer.nim57
-rw-r--r--src/io/cell.nim4
-rw-r--r--src/js/javascript.nim1
-rw-r--r--src/layout/box.nim13
-rw-r--r--src/layout/engine.nim102
-rw-r--r--src/types/url.nim22
12 files changed, 419 insertions, 221 deletions
diff --git a/readme.md b/readme.md
index 10f4eab5..27c8cf94 100644
--- a/readme.md
+++ b/readme.md
@@ -18,18 +18,16 @@ It includes its own parsers, pager, and layout engine.
 
 Currently implemented features are:
 
-* an unicode, double-width capable pager
-* a (basic) CSS-capable layout engine
-* incomplete support for forms
-* incomplete support for remote resources (i.e. css)
+* a unicode, double-width capable pager
+* a CSS-capable layout engine
+* forms
 
 **Planned** features, roughly in order of importance:
 
 * partial re-rendering of page layout
-* full support for forms
 * unicode normalization
 * non-unicode charsets (they currently crash the browser)
-* fast streams (aka pipes; use library?)
+* fast streams (aka pipelines; use library?)
 * standard-compliant support for remote resources
 * incremental page loading (to function as a proper pager)
 * table
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
diff --git a/src/html/dom.nim b/src/html/dom.nim
index 98ed023b..2c6b9134 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -84,8 +84,7 @@ type
     classList*: seq[string]
     attributes*: Table[string, string]
     hover*: bool
-    cssapplied*: bool
-    rendered*: bool
+    invalid*: bool
 
   HTMLElement* = ref object of Element
 
@@ -778,7 +777,7 @@ proc resetElement*(element: Element) =
     of INPUT_FILE:
       input.file = none(Url)
     else: discard
-    input.rendered = false
+    input.invalid = true
   of TAG_SELECT:
     let select = HTMLSelectElement(element)
     if not select.attrb("multiple"):
@@ -896,7 +895,7 @@ proc append*(parent, node: Node) =
 proc reset*(form: HTMLFormElement) =
   for control in form.controls:
     control.resetElement()
-    control.rendered = false
+    control.invalid = true
 
 proc appendAttribute*(element: Element, k, v: string) =
   case k
diff --git a/src/io/buffer.nim b/src/io/buffer.nim
index b3022615..dc8df009 100644
--- a/src/io/buffer.nim
+++ b/src/io/buffer.nim
@@ -6,7 +6,6 @@ import tables
 import terminal
 import unicode
 
-import css/cascade
 import css/sheet
 import css/stylednode
 import html/dom
@@ -61,7 +60,7 @@ type
     istream*: Stream
     streamclosed*: bool
     source*: string
-    prevnode*: Node
+    prevnode*: StyledNode
     sourcepair*: Buffer
     prev*: Buffer
     next*: Buffer
@@ -219,25 +218,27 @@ func currentDisplayCell(buffer: Buffer): FixedCell =
   let row = (buffer.cursory - buffer.fromy) * buffer.width
   return buffer.display[row + buffer.currentCellOrigin()]
 
-func getLink(node: Node): HTMLAnchorElement =
+func getLink(node: StyledNode): HTMLAnchorElement =
   if node == nil:
     return nil
-  if node.nodeType == ELEMENT_NODE and Element(node).tagType == TAG_A:
-    return HTMLAnchorElement(node)
-  return HTMLAnchorElement(node.findAncestor({TAG_A}))
+  if node.t == STYLED_ELEMENT and node.node != nil and Element(node.node).tagType == TAG_A:
+    return HTMLAnchorElement(node.node)
+  if node.node != nil:
+    return HTMLAnchorElement(node.node.findAncestor({TAG_A}))
+  #TODO ::before links?
 
 const ClickableElements = {
   TAG_A, TAG_INPUT
 }
 
-func getClickable(node: Node): Element =
-  if node == nil:
+func getClickable(styledNode: StyledNode): Element =
+  if styledNode == nil or styledNode.node == nil:
     return nil
-  if node.nodeType == ELEMENT_NODE:
-    let element = Element(node)
+  if styledNode.t == STYLED_ELEMENT:
+    let element = Element(styledNode.node)
     if element.tagType in ClickableElements:
       return element
-  return node.findAncestor(ClickableElements)
+  styledNode.node.findAncestor(ClickableElements)
 
 func getCursorClickable(buffer: Buffer): Element =
   return buffer.currentDisplayCell().node.getClickable()
@@ -693,7 +694,7 @@ proc gotoAnchor*(buffer: Buffer) =
     var i = 0
     while i < line.formats.len:
       let format = line.formats[i]
-      if anchor in format.node:
+      if format.node != nil and anchor in format.node.node:
         buffer.setCursorY(y)
         buffer.centerLine()
         buffer.setCursorX(format.pos)
@@ -794,14 +795,13 @@ proc updateHover(buffer: Buffer) =
   let thisnode = buffer.currentDisplayCell().node
   let prevnode = buffer.prevnode
 
-  if thisnode != prevnode:
-    for node in thisnode.branch:
-      if node.nodeType == ELEMENT_NODE:
-        let elem = Element(node)
-        if not elem.hover and node notin prevnode:
+  if thisnode != prevnode and (thisnode == nil or prevnode == nil or thisnode.node != prevnode.node):
+    for styledNode in thisnode.branch:
+      if styledNode.t == STYLED_ELEMENT and styledNode.node != nil:
+        let elem = Element(styledNode.node)
+        if not elem.hover:
           elem.hover = true
           buffer.reshape = true
-          elem.refreshStyle()
 
     let link = thisnode.getLink()
     if link != nil:
@@ -809,13 +809,12 @@ proc updateHover(buffer: Buffer) =
     else:
       buffer.hovertext = ""
 
-    for node in prevnode.branch:
-      if node.nodeType == ELEMENT_NODE:
-        let elem = Element(node)
-        if elem.hover and node notin thisnode:
+    for styledNode in prevnode.branch:
+      if styledNode.t == STYLED_ELEMENT and styledNode.node != nil:
+        let elem = Element(styledNode.node)
+        if elem.hover:
           elem.hover = false
           buffer.reshape = true
-          elem.refreshStyle()
 
   buffer.prevnode = thisnode
 
@@ -1121,7 +1120,7 @@ proc click*(buffer: Buffer): Option[ClickAction] =
         let status = readLine("SEARCH: ", value, buffer.width, {'\r', '\n'})
         if status:
           input.value = value
-          input.rendered = false
+          input.invalid = true
           buffer.reshape = true
         if input.form != nil:
           let submitaction = submitForm(input.form, input)
@@ -1133,7 +1132,7 @@ proc click*(buffer: Buffer): Option[ClickAction] =
         let status = readLine("TEXT: ", value, buffer.width, {'\r', '\n'})
         if status:
           input.value = value
-          input.rendered = false
+          input.invalid = true
           buffer.reshape = true
       of INPUT_FILE:
         var path = if input.file.issome:
@@ -1148,18 +1147,18 @@ proc click*(buffer: Buffer): Option[ClickAction] =
           let path = parseUrl(path, cdir)
           if path.issome:
             input.file = path
-            input.rendered = false
+            input.invalid = true
             buffer.reshape = true
       of INPUT_CHECKBOX:
         input.checked = not input.checked
-        input.rendered = false
+        input.invalid = true
         buffer.reshape = true
       of INPUT_RADIO:
         for radio in input.radiogroup:
           radio.checked = false
-          radio.rendered = false
+          radio.invalid = true
         input.checked = true
-        input.rendered = false
+        input.invalid = true
         buffer.reshape = true
       of INPUT_RESET:
         if input.form != nil:
diff --git a/src/io/cell.nim b/src/io/cell.nim
index 4d808dda..80ada720 100644
--- a/src/io/cell.nim
+++ b/src/io/cell.nim
@@ -4,7 +4,7 @@ import strutils
 import sugar
 import unicode
 
-import html/dom
+import css/stylednode
 import layout/box
 import types/color
 import utils/twtstr
@@ -26,7 +26,7 @@ type
 
   Cell* = object of RootObj
     format*: Format
-    node*: Node
+    node*: StyledNode
 
   FormatCell* = object of Cell
     pos*: int
diff --git a/src/js/javascript.nim b/src/js/javascript.nim
index 95e37110..c728283a 100644
--- a/src/js/javascript.nim
+++ b/src/js/javascript.nim
@@ -98,7 +98,6 @@ proc setFunctionProperty*(obj: JSObject, name: string, fun: JSCFunction) =
   #), cstring(name), 1))
 
 proc free*(ctx: var JSContext) =
-  eprint "free"
   let opaque = ctx.getOpaque()
   if opaque != nil:
     dealloc(opaque)
diff --git a/src/layout/box.nim b/src/layout/box.nim
index 7e0ac948..1626791d 100644
--- a/src/layout/box.nim
+++ b/src/layout/box.nim
@@ -1,5 +1,6 @@
 import options
 
+import css/stylednode
 import css/values
 import html/dom
 import io/term
@@ -28,8 +29,7 @@ type
     children*: seq[BoxBuilder]
     inlinelayout*: bool
     computed*: CSSComputedValues
-    node*: Node
-    element*: Element
+    node*: StyledNode
 
   InlineBoxBuilder* = ref object of BoxBuilder
     text*: seq[string]
@@ -48,9 +48,12 @@ type
     marker*: MarkerBoxBuilder
     content*: BlockBoxBuilder
 
+  TableRowGroupBoxBuilder* = ref object of BoxBuilder
+
+  TableRowBoxBuilder* = ref object of BoxBuilder
+
   TableBoxBuilder* = ref object of BoxBuilder
-    inline*: bool
-    content*: BlockBoxBuilder
+    rowgroups*: seq[TableRowGroupBoxBuilder]
 
   InlineAtom* = ref object of RootObj
     offset*: Offset
@@ -66,7 +69,7 @@ type
     fontweight*: int
     textdecoration*: CSSTextDecoration
     color*: CSSColor
-    node*: Node
+    node*: StyledNode
 
   InlineSpacing* = ref object of InlineAtom
     format*: ComputedFormat
diff --git a/src/layout/engine.nim b/src/layout/engine.nim
index 8ec1c668..18af9e7b 100644
--- a/src/layout/engine.nim
+++ b/src/layout/engine.nim
@@ -16,7 +16,7 @@ func px(l: CSSLength, viewport: Viewport, p = 0): int {.inline.} =
 type InlineState = object
   ictx: InlineContext
   skip: bool
-  node: Node
+  node: StyledNode
   word: InlineWord
   maxwidth: int
   computed: CSSComputedValues
@@ -284,7 +284,7 @@ proc processWhitespace(state: var InlineState, c: char) =
     else:
       inc state.ictx.whitespacenum
 
-proc renderText*(ictx: InlineContext, str: string, maxwidth: int, computed: CSSComputedValues, node: Node) =
+proc renderText*(ictx: InlineContext, str: string, maxwidth: int, computed: CSSComputedValues, node: StyledNode) =
   var state: InlineState
   state.computed = computed
   state.ictx = ictx
@@ -421,7 +421,7 @@ proc positionInlines(bctx: BlockBox) =
 
 proc buildBlock(box: BlockBoxBuilder, parent: BlockBox): BlockBox
 proc buildInlines(bctx: BlockBox, inlines: seq[BoxBuilder]): InlineContext
-proc buildBlocks(bctx: BlockBox, blocks: seq[BoxBuilder], node: Node)
+proc buildBlocks(bctx: BlockBox, blocks: seq[BoxBuilder], node: StyledNode)
 
 proc applyInlineDimensions(bctx: BlockBox) =
   bctx.height += bctx.inline.height
@@ -436,7 +436,7 @@ proc buildInlineLayout(bctx: BlockBox, children: seq[BoxBuilder]) =
   bctx.positionInlines()
 
 # Builder only contains block boxes.
-proc buildBlockLayout(bctx: BlockBox, children: seq[BoxBuilder], node: Node) =
+proc buildBlockLayout(bctx: BlockBox, children: seq[BoxBuilder], node: StyledNode) =
   bctx.buildBlocks(children, node)
 
 func baseline(bctx: BlockBox): int =
@@ -609,13 +609,13 @@ proc positionBlocks(bctx: BlockBox) =
   bctx.width += bctx.padding_left
   bctx.width += bctx.padding_right
 
-proc buildBlocks(bctx: BlockBox, blocks: seq[BoxBuilder], node: Node) =
+proc buildBlocks(bctx: BlockBox, blocks: seq[BoxBuilder], node: StyledNode) =
   for child in blocks:
     var cblock: BlockBox
     case child.computed{"display"}
     of DISPLAY_BLOCK: cblock = buildBlock(BlockBoxBuilder(child), bctx)
     of DISPLAY_LIST_ITEM: cblock = buildListItem(ListItemBoxBuilder(child), bctx)
-    of DISPLAY_TABLE: cblock = buildBlock(TableBoxBuilder(child).content, bctx)
+    of DISPLAY_TABLE: cblock = buildBlock(BlockBoxBuilder(child), bctx)
     else: assert false, "child.t is " & $child.computed{"display"}
     bctx.nested.add(cblock)
   bctx.positionBlocks()
@@ -682,9 +682,19 @@ proc getTableBox(computed: CSSComputedValues): TableBoxBuilder =
   new(result)
   result.computed = computed.copyProperties()
 
+# Also known as <tbody>.
+proc getTableRowGroupBox(computed: CSSComputedValues): TableRowGroupBoxBuilder =
+  new(result)
+  result.computed = computed.copyProperties()
+
+proc getTableRowBox(computed: CSSComputedValues): TableRowBoxBuilder =
+  new(result)
+  result.computed = computed.copyProperties()
+
 type BlockGroup = object
   parent: BlockBoxBuilder
   boxes: seq[BoxBuilder]
+  listItemCounter: int
 
 proc add(blockgroup: var BlockGroup, box: BoxBuilder) {.inline.} =
   blockgroup.boxes.add(box)
@@ -703,8 +713,6 @@ func canGenerateAnonymousInline(blockgroup: BlockGroup, computed: CSSComputedVal
     computed{"white-space"} in {WHITESPACE_PRE_LINE, WHITESPACE_PRE, WHITESPACE_PRE_WRAP} or
     not str.onlyWhitespace()
 
-proc generateBlockBox(styledNode: StyledNode, viewport: Viewport, marker = none(MarkerBoxBuilder)): BlockBoxBuilder
-
 template flush_ibox() =
   if ibox != nil:
     assert ibox.computed{"display"} in {DISPLAY_INLINE, DISPLAY_INLINE_BLOCK}
@@ -713,10 +721,13 @@ template flush_ibox() =
 
 proc newBlockGroup(parent: BlockBoxBuilder): BlockGroup =
   result.parent = parent
+  result.listItemCounter = 1
+
+proc generateBlockBox(styledNode: StyledNode, viewport: Viewport, marker = none(MarkerBoxBuilder)): BlockBoxBuilder
 
-proc generateInlineBoxes(box: BlockBoxBuilder, styledNode: StyledNode, blockgroup: var BlockGroup, viewport: Viewport, listItemCounter: var int)
+proc generateInlineBoxes(box: BlockBoxBuilder, styledNode: StyledNode, blockgroup: var BlockGroup, viewport: Viewport)
 
-proc generateFromElem(styledNode: StyledNode, blockgroup: var BlockGroup, viewport: Viewport, ibox: var InlineBoxBuilder, listItemCounter: var int) =
+proc generateFromElem(styledNode: StyledNode, blockgroup: var BlockGroup, viewport: Viewport, ibox: var InlineBoxBuilder) =
   let box = blockgroup.parent
   if styledNode.node != nil:
     let elem = Element(styledNode.node)
@@ -732,17 +743,17 @@ proc generateFromElem(styledNode: StyledNode, blockgroup: var BlockGroup, viewpo
     box.children.add(childbox)
   of DISPLAY_LIST_ITEM:
     blockgroup.flush()
-    let childbox = getListItemBox(styledNode.computed, listItemCounter)
+    let childbox = getListItemBox(styledNode.computed, blockgroup.listItemCounter)
     if childbox.computed{"list-style-position"} == LIST_STYLE_POSITION_INSIDE:
       childbox.content = styledNode.generateBlockBox(viewport, some(childbox.marker))
       childbox.marker = nil
     else:
       childbox.content = styledNode.generateBlockBox(viewport)
     box.children.add(childbox)
-    inc listItemCounter
+    inc blockgroup.listItemCounter
   of DISPLAY_INLINE:
     flush_ibox
-    box.generateInlineBoxes(styledNode, blockgroup, viewport, listItemCounter)
+    box.generateInlineBoxes(styledNode, blockgroup, viewport)
   of DISPLAY_INLINE_BLOCK:
     flush_ibox
     let childbox = getInlineBlockBox(styledNode.computed)
@@ -750,25 +761,24 @@ proc generateFromElem(styledNode: StyledNode, blockgroup: var BlockGroup, viewpo
     blockgroup.add(childbox)
   of DISPLAY_TABLE:
     blockgroup.flush()
-    let childbox = getTableBox(styledNode.computed)
-    childbox.content = styledNode.generateBlockBox(viewport)
-    box.children.add(childbox)
+    #styledNode.generateFromElemTable(blockgroup.parent, viewport)
   of DISPLAY_TABLE_ROW_GROUP:
     discard
+  of DISPLAY_NONE: discard
   else:
     discard #TODO
 
-proc generateInlineBoxes(box: BlockBoxBuilder, styledNode: StyledNode, blockgroup: var BlockGroup, viewport: Viewport, listItemCounter: var int) =
+proc generateInlineBoxes(box: BlockBoxBuilder, styledNode: StyledNode, blockgroup: var BlockGroup, viewport: Viewport) =
   var ibox: InlineBoxBuilder = nil
 
   for child in styledNode.children:
     case child.t
     of STYLED_ELEMENT:
-      generateFromElem(child, blockgroup, viewport, ibox, listItemCounter)
+      generateFromElem(child, blockgroup, viewport, ibox)
     of STYLED_TEXT:
       if ibox == nil:
         ibox = getTextBox(styledNode.computed)
-        ibox.node = styledNode.node
+        ibox.node = styledNode
       ibox.text.add(child.text)
 
   flush_ibox
@@ -777,7 +787,6 @@ proc generateBlockBox(styledNode: StyledNode, viewport: Viewport, marker = none(
   let box = getBlockBox(styledNode.computed)
   var blockgroup = newBlockGroup(box)
   var ibox: InlineBoxBuilder = nil
-  var listItemCounter = 1 # ordinal value of current list
 
   if marker.issome:
     ibox = marker.get
@@ -787,12 +796,12 @@ proc generateBlockBox(styledNode: StyledNode, viewport: Viewport, marker = none(
     case child.t
     of STYLED_ELEMENT:
       flush_ibox
-      generateFromElem(child, blockgroup, viewport, ibox, listItemCounter)
+      generateFromElem(child, blockgroup, viewport, ibox)
     of STYLED_TEXT:
       if canGenerateAnonymousInline(blockgroup, box.computed, child.text):
         if ibox == nil:
           ibox = getTextBox(styledNode.computed)
-          ibox.node = styledNode.node
+          ibox.node = styledNode
         ibox.text.add(child.text)
 
   flush_ibox
@@ -805,6 +814,55 @@ proc generateBlockBox(styledNode: StyledNode, viewport: Viewport, marker = none(
       blockgroup.flush()
   return box
 
+const RowGroupBox = {DISPLAY_TABLE_ROW_GROUP, DISPLAY_TABLE_HEADER_GROUP,
+                     DISPLAY_TABLE_FOOTER_GROUP}
+const ProperTableChild = {DISPLAY_TABLE_ROW, DISPLAY_TABLE_COLUMN,
+                          DISPLAY_TABLE_COLUMN_GROUP} + RowGroupBox
+const ProperTableRowParent = {DISPLAY_TABLE} + RowGroupBox #TODO inline-table box
+const InternalTableBox = {DISPLAY_TABLE_CELL, DISPLAY_TABLE_ROW, DISPLAY_TABLE_COLUMN, DISPLAY_TABLE_COLUMN_GROUP} + RowGroupBox
+const TabularContainer = {DISPLAY_TABLE_ROW} + ProperTableRowParent
+
+proc generateTableBox(styledNode: StyledNode, viewport: Viewport): TableBox =
+  discard
+  #let box = getTableBox(styledNode.computed)
+  #var blockgroup = newBlockGroup(box)
+  #var ibox: InlineBoxBuilder = nil
+  #var listItemCounter = 1
+
+  #for child in styledNode.children:
+  #  if child.t == STYLED_ELEMENT:
+  #    generateFromElem(child, blockgroup, viewport, ibox)
+  #  else:
+  #    if canGenerateAnonymousInline(blockgroup, box.computed, child.text):
+  #      if ibox == nil:
+  #        ibox = getTextBox(styledNode.computed)
+  #        ibox.node = styledNode.node
+  #      ibox.text.add(child.text)
+
+  #flush_ibox
+  #blockgroup.flush()
+
+  ## Generate missing child wrappers
+  #var anonRow: TableRowBoxBuilder
+  #for child in box.children:
+  #  if child.computed{"display"} notin ProperTableChild:
+  #    if anonRow != nil:
+  #      anonRow = getTableRowBox(box.computed.inheritProperties())
+  #    discard
+
+
+#proc generateFromElemTable(styledNode: StyledNode, viewport: Viewport, table: TableBoxBuilder = nil, parent: BoxBuilder = nil): BlockBox =
+#  case styledNode.computed{"display"}
+#  of DISPLAY_TABLE:
+#  of DISPLAY_TABLE_ROW_GROUP:
+#    if parent != table:
+#      # misparented
+#      let box = getTableRowGroupBox(styledNode.computed)
+#      let anonymousTable = getTableBox(styledNode.computed.inheritProperties())
+#      anonymousTable.children.add(box)
+#      parent.children.add(anonymousTable)
+#  else:
+#    discard
 
 proc renderLayout*(viewport: var Viewport, document: Document, root: StyledNode) =
   let builder = root.generateBlockBox(viewport)
diff --git a/src/types/url.nim b/src/types/url.nim
index 4099fed9..afe7d83a 100644
--- a/src/types/url.nim
+++ b/src/types/url.nim
@@ -488,18 +488,16 @@ proc basicParseUrl*(input: string, base = none(Url), url: var Url = Url(), overr
         #TODO validation error
         if atsignseen:
           buffer = "%40" & buffer
-          atsignseen = true
-          var i = 0
-          while i < buffer.len:
-            if c == ':' and not passwordtokenseen:
-              passwordtokenseen = true
-              inc i
-              continue
-            if passwordtokenseen:
-              url.password.percentEncode(c, UserInfoPercentEncodeSet)
-            else:
-              url.username.percentEncode(c, UserInfoPercentEncodeSet)
-          buffer = ""
+        atsignseen = true
+        for c in buffer:
+          if c == ':' and not passwordtokenseen:
+            passwordtokenseen = true
+            continue
+          if passwordtokenseen:
+            url.password.percentEncode(c, UserInfoPercentEncodeSet)
+          else:
+            url.username.percentEncode(c, UserInfoPercentEncodeSet)
+        buffer = ""
       elif not has or c in {'/', '?', '#'} or (url.is_special and c == '\\'):
         if atsignseen and buffer == "":
           #TODO validation error