about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2025-01-20 18:08:12 +0100
committerbptato <nincsnevem662@gmail.com>2025-01-20 19:34:27 +0100
commit66b18695de1e6629341e08faf487a693cc26211a (patch)
tree23e5d44a803051d34040278bdc245cc92fbc418e
parent2291b75ed7133b77bce37fcc07500d5081682892 (diff)
downloadchawan-66b18695de1e6629341e08faf487a693cc26211a.tar.gz
cascade: collapse StyledNode tree into DOM
We now compute styles on-demand, which is both more efficient and
simpler than the convoluted tree diffing logic we previously used.
-rw-r--r--src/css/cascade.nim399
-rw-r--r--src/css/cssvalues.nim5
-rw-r--r--src/css/layout.nim19
-rw-r--r--src/css/match.nim2
-rw-r--r--src/css/selectorparser.nim10
-rw-r--r--src/css/stylednode.nim72
-rw-r--r--src/html/chadombuilder.nim5
-rw-r--r--src/html/dom.nim269
-rw-r--r--src/server/buffer.nim70
9 files changed, 317 insertions, 534 deletions
diff --git a/src/css/cascade.nim b/src/css/cascade.nim
index 0b4369df..7fff1434 100644
--- a/src/css/cascade.nim
+++ b/src/css/cascade.nim
@@ -7,14 +7,13 @@ import css/cssparser
 import css/cssvalues
 import css/lunit
 import css/match
-import css/mediaquery
 import css/selectorparser
 import css/sheet
-import css/stylednode
 import html/catom
 import html/dom
 import html/enums
 import html/script
+import types/bitmap
 import types/color
 import types/jscolor
 import types/opt
@@ -46,6 +45,7 @@ type
 proc applyValue(vals: CSSValues; entry: CSSComputedEntry;
   parentComputed, previousOrigin: CSSValues; initMap: var InitMap;
   initType: InitType; nextInitType: set[InitType]; window: Window)
+proc applyStyle*(element: Element)
 
 proc calcRule(tosorts: var ToSorts; element: Element;
     depends: var DependencyInfo; rule: CSSRuleDef) =
@@ -63,9 +63,8 @@ proc add(entry: var RuleListEntry; rule: CSSRuleDef) =
   entry.normalVars.add(rule.normalVars)
   entry.importantVars.add(rule.importantVars)
 
-proc calcRules(map: var RuleListMap; styledNode: StyledNode;
-    sheet: CSSStylesheet; origin: CSSOrigin) =
-  let element = styledNode.element
+proc calcRules(map: var RuleListMap; element: Element;
+    sheet: CSSStylesheet; origin: CSSOrigin; depends: var DependencyInfo) =
   var rules: seq[CSSRuleDef] = @[]
   sheet.tagTable.withValue(element.localName, v):
     rules.add(v[])
@@ -82,7 +81,7 @@ proc calcRules(map: var RuleListMap; styledNode: StyledNode;
     rules.add(rule)
   var tosorts = ToSorts.default
   for rule in rules:
-    tosorts.calcRule(element, styledNode.element.depends, rule)
+    tosorts.calcRule(element, depends, rule)
   for pseudo, it in tosorts.mpairs:
     it.sort(proc(x, y: RulePair): int =
       let n = cmp(x.specificity, y.specificity)
@@ -278,12 +277,14 @@ proc applyPresHints(computed: CSSValues; element: Element;
       set_cv cptHeight, length, resolveLength(cuEm, float32(size), attrs)
   else: discard
 
-proc applyDeclarations0(rules: RuleList; parent, element: Element;
+proc applyDeclarations(rules: RuleList; parent, element: Element;
     window: Window): CSSValues =
   result = CSSValues()
   var parentComputed: CSSValues = nil
   var parentVars: CSSVariableMap = nil
   if parent != nil:
+    if parent.computed == nil:
+      parent.applyStyle()
     parentComputed = parent.computed
     parentVars = parentComputed.vars
   for origin in CSSOrigin:
@@ -352,25 +353,17 @@ func hasValues(rules: RuleList): bool =
       return true
   return false
 
-func applyMediaQuery(ss: CSSStylesheet; window: Window): CSSStylesheet =
-  if ss == nil:
-    return nil
-  var res = CSSStylesheet()
-  res[] = ss[]
-  for mq in ss.mqList:
-    if mq.query.applies(window.settings.scripting, window.attrsp):
-      res.add(mq.children.applyMediaQuery(window))
-  return res
-
-proc applyDeclarations(styledNode: StyledNode; parent: Element;
-    window: Window; ua, user: CSSStylesheet; author: seq[CSSStylesheet]) =
+proc applyStyle*(element: Element) =
+  let document = element.document
+  let window = document.window
+  var depends = DependencyInfo.default
   var map = RuleListMap.default
-  map.calcRules(styledNode, ua, coUserAgent)
-  if user != nil:
-    map.calcRules(styledNode, user, coUser)
-  for rule in author:
-    map.calcRules(styledNode, rule, coAuthor)
-  let style = styledNode.element.cachedStyle
+  for sheet in document.uaSheets:
+    map.calcRules(element, sheet, coUserAgent, depends)
+  map.calcRules(element, document.userSheet, coUser, depends)
+  for sheet in document.authorSheets:
+    map.calcRules(element, sheet, coAuthor, depends)
+  let style = element.cachedStyle
   if window.styling and style != nil:
     for decl in style.decls:
       #TODO variables
@@ -380,263 +373,111 @@ proc applyDeclarations(styledNode: StyledNode; parent: Element;
         map[peNone][coAuthor].important.add(vals)
       else:
         map[peNone][coAuthor].normal.add(vals)
-  let element = styledNode.element
-  element.computedMap[peNone] = map[peNone].applyDeclarations0(parent, element,
-    window)
+  element.applyStyleDependencies(depends)
+  element.computedMap[peNone] =
+    map[peNone].applyDeclarations(element.parentElement, element, window)
   for pseudo in peBefore..peAfter:
     if map[pseudo].hasValues() or window.settings.scripting == smApp:
-      let computed = map[pseudo].applyDeclarations0(element, nil, window)
+      let computed = map[pseudo].applyDeclarations(element, nil, window)
       element.computedMap[pseudo] = computed
 
-type CascadeFrame = object
-  styledParent: StyledNode
-  child: Node
-  pseudo: PseudoElement
-  cachedChild: StyledNode
-  cachedChildren: seq[StyledNode]
-
-proc getAuthorSheets(document: Document): seq[CSSStylesheet] =
-  var author: seq[CSSStylesheet] = @[]
-  for sheet in document.sheets():
-    author.add(sheet.applyMediaQuery(document.window))
-  return author
-
-proc applyRulesFrameValid(frame: var CascadeFrame): StyledNode =
-  let styledParent = frame.styledParent
-  let cachedChild = frame.cachedChild
-  # Pseudo elements can't have invalid children.
-  if cachedChild.t == stElement and cachedChild.pseudo == peNone:
-    # Refresh child nodes:
-    # * move old seq to a temporary location in frame
-    # * create new seq, assuming capacity == len of the previous pass
-    frame.cachedChildren = move(cachedChild.children)
-    cachedChild.children = newSeqOfCap[StyledNode](frame.cachedChildren.len)
-  if styledParent != nil:
-    styledParent.children.add(cachedChild)
-  return cachedChild
-
-proc applyRulesFrameInvalid(frame: CascadeFrame; ua, user: CSSStylesheet;
-    author: seq[CSSStylesheet]; window: Window): StyledNode =
-  let pseudo = frame.pseudo
-  let styledParent = frame.styledParent
-  let child = frame.child
-  case pseudo
-  of peNone: # not a pseudo-element, but a real one
-    assert child != nil
-    if child of Element:
-      let element = Element(child)
-      let styledChild = newStyledElement(element)
-      if styledParent == nil: # root element
-        styledChild.applyDeclarations(nil, window, ua, user, author)
-      else:
-        styledParent.children.add(styledChild)
-        styledChild.applyDeclarations(styledParent.element, window, ua, user,
-          author)
-      return styledChild
-    elif child of Text:
-      let text = Text(child)
-      let styledChild = styledParent.newStyledText(text)
-      styledParent.children.add(styledChild)
-      return styledChild
-  of peBefore, peAfter:
-    let parent = styledParent.element
-    if parent.computedMap[pseudo] != nil and
-        parent.computedMap[pseudo]{"content"}.len > 0:
-      let styledPseudo = styledParent.newStyledElement(pseudo)
-      for content in parent.computedMap[pseudo]{"content"}:
-        let child = styledPseudo.newStyledReplacement(content, peNone)
-        styledPseudo.children.add(child)
-      styledParent.children.add(styledPseudo)
-  of peInputText:
-    let s = HTMLInputElement(styledParent.element).inputString()
-    if s.len > 0:
-      let content = styledParent.element.document.newText(s)
-      let styledText = styledParent.newStyledText(content)
-      # Note: some pseudo-elements (like input text) generate text nodes
-      # directly, so we have to cache them like this.
-      styledText.pseudo = pseudo
-      styledParent.children.add(styledText)
-  of peTextareaText:
-    let s = HTMLTextAreaElement(styledParent.element).textAreaString()
-    if s.len > 0:
-      let content = styledParent.element.document.newText(s)
-      let styledText = styledParent.newStyledText(content)
-      styledText.pseudo = pseudo
-      styledParent.children.add(styledText)
-  of peImage:
-    let content = CSSContent(
-      t: ContentImage,
-      bmp: HTMLImageElement(styledParent.element).bitmap
-    )
-    let styledText = styledParent.newStyledReplacement(content, pseudo)
-    styledParent.children.add(styledText)
-  of peSVG:
-    let content = CSSContent(
-      t: ContentImage,
-      bmp: SVGSVGElement(styledParent.element).bitmap
-    )
-    let styledText = styledParent.newStyledReplacement(content, pseudo)
-    styledParent.children.add(styledText)
-  of peCanvas:
-    let bmp = HTMLCanvasElement(styledParent.element).bitmap
-    if bmp != nil and bmp.cacheId != 0:
-      let content = CSSContent(
-        t: ContentImage,
-        bmp: bmp
-      )
-      let styledText = styledParent.newStyledReplacement(content, pseudo)
-      styledParent.children.add(styledText)
-  of peVideo:
-    let content = CSSContent(t: ContentVideo)
-    let styledText = styledParent.newStyledReplacement(content, pseudo)
-    styledParent.children.add(styledText)
-  of peAudio:
-    let content = CSSContent(t: ContentAudio)
-    let styledText = styledParent.newStyledReplacement(content, pseudo)
-    styledParent.children.add(styledText)
-  of peIFrame:
-    let content = CSSContent(t: ContentIFrame)
-    let styledText = styledParent.newStyledReplacement(content, pseudo)
-    styledParent.children.add(styledText)
-  of peNewline:
-    let content = CSSContent(t: ContentNewline)
-    let styledText = styledParent.newStyledReplacement(content, pseudo)
-    styledParent.children.add(styledText)
-  return nil
-
-proc stackAppend(styledStack: var seq[CascadeFrame]; frame: CascadeFrame;
-    styledParent: StyledNode; child: Element; i: var int) =
-  var cached: StyledNode = nil
-  if frame.cachedChildren.len > 0:
-    for j in countdown(i, 0):
-      let it = frame.cachedChildren[j]
-      if it.t == stElement and it.pseudo == peNone and it.element == child:
-        i = j - 1
-        cached = it
-        break
-  styledStack.add(CascadeFrame(
-    styledParent: styledParent,
-    child: child,
-    pseudo: peNone,
-    cachedChild: cached
-  ))
-
-proc stackAppend(styledStack: var seq[CascadeFrame]; frame: CascadeFrame;
-    styledParent: StyledNode; child: Text; i: var int) =
-  var cached: StyledNode = nil
-  if frame.cachedChildren.len > 0:
-    for j in countdown(i, 0):
-      let it = frame.cachedChildren[j]
-      if it.t == stText and it.text == child:
-        i = j - 1
-        cached = it
-        break
-  styledStack.add(CascadeFrame(
-    styledParent: styledParent,
-    child: child,
-    pseudo: peNone,
-    cachedChild: cached
-  ))
-
-proc stackAppend(styledStack: var seq[CascadeFrame]; frame: CascadeFrame;
-    styledParent: StyledNode; pseudo: PseudoElement; i: var int) =
-  # 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:
-    var cached: StyledNode = nil
-    for j in countdown(i, 0):
-      let it = frame.cachedChildren[j]
-      if it.pseudo == pseudo:
-        cached = it
-        i = j - 1
-        break
-    # When calculating pseudo-element rules, their dependencies are added
-    # to their parent's dependency list; so invalidating a pseudo-element
-    # invalidates its parent too, which in turn automatically rebuilds
-    # the pseudo-element.
-    # In other words, we can just do this:
-    if cached != nil:
-      styledStack.add(CascadeFrame(
-        styledParent: styledParent,
-        pseudo: pseudo,
-        cachedChild: cached
-      ))
-  else:
-    styledStack.add(CascadeFrame(
-      styledParent: styledParent,
-      pseudo: pseudo,
-      cachedChild: nil
-    ))
-
-# Append children to styledChild.
-proc appendChildren(styledStack: var seq[CascadeFrame]; frame: CascadeFrame;
-    styledChild: StyledNode) =
-  # i points to the child currently being inspected.
-  var idx = frame.cachedChildren.len - 1
-  let element = styledChild.element
-  # reset invalid flag here to avoid a type conversion above
-  element.invalid = false
-  styledStack.stackAppend(frame, styledChild, peAfter, idx)
-  case element.tagType
-  of TAG_TEXTAREA:
-    styledStack.stackAppend(frame, styledChild, peTextareaText, idx)
-  of TAG_IMG: styledStack.stackAppend(frame, styledChild, peImage, idx)
-  of TAG_VIDEO: styledStack.stackAppend(frame, styledChild, peVideo, idx)
-  of TAG_AUDIO: styledStack.stackAppend(frame, styledChild, peAudio, idx)
-  of TAG_BR: styledStack.stackAppend(frame, styledChild, peNewline, idx)
-  of TAG_CANVAS: styledStack.stackAppend(frame, styledChild, peCanvas, idx)
-  of TAG_IFRAME: styledStack.stackAppend(frame, styledChild, peIFrame, idx)
-  elif element.tagType(Namespace.SVG) == TAG_SVG:
-    styledStack.stackAppend(frame, styledChild, peSVG, idx)
-  else:
-    for i in countdown(element.childList.high, 0):
-      let child = element.childList[i]
-      if child of Element:
-        styledStack.stackAppend(frame, styledChild, Element(child), idx)
-      elif child of Text:
-        styledStack.stackAppend(frame, styledChild, Text(child), idx)
-    if element.tagType == TAG_INPUT:
-      styledStack.stackAppend(frame, styledChild, peInputText, idx)
-  styledStack.stackAppend(frame, styledChild, peBefore, idx)
-
-# Builds a StyledNode tree, optionally based on a previously cached version.
-proc applyRules(document: Document; ua, user: CSSStylesheet;
-    cachedTree: StyledNode): StyledNode =
-  let html = document.documentElement
-  if html == nil:
+# Abstraction over the DOM to pretend that elements, text, replaced and
+# pseudo-elements are derived from the same type.
+type
+  StyledType* = enum
+    stElement, stText, stReplacement
+
+  StyledNode* = object
+    element*: Element
+    pseudo*: PseudoElement
+    case t*: StyledType
+    of stText:
+      text*: CharacterData
+    of stElement:
+      discard
+    of stReplacement:
+      # replaced elements: quotes, images, or (TODO) markers
+      content*: CSSContent
+
+when defined(debug):
+  func `$`*(node: StyledNode): string =
+    case node.t
+    of stText:
+      return "#text " & node.text.data
+    of stElement:
+      if node.pseudo != peNone:
+        return $node.element.tagType & "::" & $node.pseudo
+      return $node.element
+    of stReplacement:
+      return "#replacement"
+
+# Defined here so it isn't accidentally used in dom.
+#TODO it may be better to do it in dom anyway, so we can cache it...
+func newCharacterData*(data: sink string): CharacterData =
+  return CharacterData(data: data)
+
+template computed*(styledNode: StyledNode): CSSValues =
+  styledNode.element.computedMap[styledNode.pseudo]
+
+proc initStyledElement*(element: Element): StyledNode =
+  if element.computed == nil:
+    element.applyStyle()
+  return StyledNode(t: stElement, element: element)
+
+proc initStyledReplacement(parent: Element; content: sink CSSContent):
+    StyledNode =
+  return StyledNode(t: stReplacement, element: parent, content: content)
+
+proc initStyledImage(parent: Element; bmp: NetworkBitmap): StyledNode =
+  return initStyledReplacement(parent, CSSContent(t: ContentImage, bmp: bmp))
+
+proc initStyledPseudo(parent: Element; pseudo: PseudoElement): StyledNode =
+  return StyledNode(t: stElement, pseudo: pseudo, element: parent)
+
+proc initStyledText(parent: Element; text: CharacterData): StyledNode =
+  return StyledNode(t: stText, element: parent, text: text)
+
+proc initStyledText(parent: Element; s: sink string): StyledNode =
+  return initStyledText(parent, newCharacterData(s))
+
+# Many yields; we use a closure iterator to avoid bloating the code.
+iterator children*(styledNode: StyledNode): StyledNode {.closure.} =
+  if styledNode.t != stElement:
     return
-  let author = document.getAuthorSheets()
-  var styledStack = @[CascadeFrame(
-    child: html,
-    pseudo: peNone,
-    cachedChild: cachedTree
-  )]
-  var root: StyledNode = nil
-  var toReset: seq[Element] = @[]
-  while styledStack.len > 0:
-    var frame = styledStack.pop()
-    let styledParent = frame.styledParent
-    let valid = frame.cachedChild != nil and frame.cachedChild.isValid(toReset)
-    let styledChild = if valid:
-      frame.applyRulesFrameValid()
+  if styledNode.pseudo == peNone:
+    let parent = styledNode.element
+    if parent.computedMap[peBefore] != nil and
+        parent.computedMap[peBefore]{"content"}.len > 0:
+      yield initStyledPseudo(parent, peBefore)
+    case parent.tagType
+    of TAG_INPUT:
+      #TODO cache (just put value in a CharacterData)
+      let s = HTMLInputElement(parent).inputString()
+      if s.len > 0:
+        yield initStyledText(parent, s)
+    of TAG_TEXTAREA:
+      #TODO cache (do the same as with input, and add borders in render)
+      yield initStyledText(parent, HTMLTextAreaElement(parent).textAreaString())
+    of TAG_IMG: yield initStyledImage(parent, HTMLImageElement(parent).bitmap)
+    of TAG_CANVAS:
+      yield initStyledImage(parent, HTMLImageElement(parent).bitmap)
+    of TAG_VIDEO: yield initStyledText(parent, "[video]")
+    of TAG_AUDIO: yield initStyledText(parent, "[audio]")
+    of TAG_BR:
+      yield initStyledReplacement(parent, CSSContent(t: ContentNewline))
+    of TAG_IFRAME: yield initStyledText(parent, "[iframe]")
+    elif parent.tagType(Namespace.SVG) == TAG_SVG:
+      yield initStyledImage(parent, SVGSVGElement(parent).bitmap)
     else:
-      # From here on, computed values of this node's children are invalid
-      # because of property inheritance.
-      frame.cachedChild = nil
-      frame.applyRulesFrameInvalid(ua, user, author, document.window)
-    if styledChild != nil:
-      if styledParent == nil:
-        # Root element
-        root = styledChild
-      if styledChild.t == stElement and styledChild.pseudo == peNone:
-        # note: following resets styledChild.node's invalid flag
-        styledStack.appendChildren(frame, styledChild)
-  for element in toReset:
-    element.invalidDeps = {}
-  return root
-
-proc applyStylesheets*(document: Document; uass, userss: CSSStylesheet;
-    previousStyled: StyledNode): StyledNode =
-  let uass = uass.applyMediaQuery(document.window)
-  let userss = userss.applyMediaQuery(document.window)
-  return document.applyRules(uass, userss, previousStyled)
+      for it in parent.childList:
+        if it of Element:
+          yield initStyledElement(Element(it))
+        elif it of Text:
+          yield initStyledText(parent, Text(it))
+    if parent.computedMap[peAfter] != nil and
+        parent.computedMap[peAfter]{"content"}.len > 0:
+      yield initStyledPseudo(parent, peAfter)
+  else:
+    let parent = styledNode.element
+    for content in parent.computedMap[styledNode.pseudo]{"content"}:
+      yield parent.initStyledReplacement(content)
diff --git a/src/css/cssvalues.nim b/src/css/cssvalues.nim
index 7da9bbc4..537f856d 100644
--- a/src/css/cssvalues.nim
+++ b/src/css/cssvalues.nim
@@ -266,9 +266,8 @@ type
     BorderCollapseCollapse = "collapse"
 
   CSSContentType* = enum
-    ContentNone, ContentString, ContentOpenQuote, ContentCloseQuote,
-    ContentNoOpenQuote, ContentNoCloseQuote, ContentImage, ContentVideo,
-    ContentAudio, ContentNewline, ContentIFrame
+    ContentString, ContentOpenQuote, ContentCloseQuote, ContentNoOpenQuote,
+    ContentNoCloseQuote, ContentImage, ContentNewline
 
   CSSFloat* = enum
     FloatNone = "none"
diff --git a/src/css/layout.nim b/src/css/layout.nim
index de255afa..471c7364 100644
--- a/src/css/layout.nim
+++ b/src/css/layout.nim
@@ -2,9 +2,9 @@ import std/algorithm
 import std/math
 
 import css/box
+import css/cascade
 import css/cssvalues
 import css/lunit
-import css/stylednode
 import html/dom
 import types/bitmap
 import types/winattrs
@@ -49,17 +49,10 @@ type
     myRootProperties: CSSValues
     # placeholder text data
     imgText: CharacterData
-    audioText: CharacterData
-    videoText: CharacterData
-    iframeText: CharacterData
     luctx: LUContext
 
 const DefaultSpan = Span(start: 0, send: LUnit.high)
 
-# Defined here so it isn't accidentally used in dom.
-func newCharacterData(data: sink string): CharacterData =
-  return CharacterData(data: data)
-
 func minWidth(sizes: ResolvedSizes): LUnit =
   return sizes.bounds.a[dtHorizontal].start
 
@@ -3158,7 +3151,6 @@ proc buildFromElem(ctx: var BlockBuilderContext; styledNode: StyledNode;
 proc buildReplacement(ctx: var BlockBuilderContext; child: StyledNode;
     parent: Element; computed: CSSValues) =
   case child.content.t
-  of ContentNone: assert false # unreachable for `content'
   of ContentOpenQuote:
     let quotes = parent.computed{"quotes"}
     let s = if quotes == nil:
@@ -3199,12 +3191,6 @@ proc buildReplacement(ctx: var BlockBuilderContext; child: StyledNode;
       ))
     else:
       ctx.pushInlineText(computed, parent, ctx.lctx.imgText)
-  of ContentVideo:
-    ctx.pushInlineText(computed, parent, ctx.lctx.videoText)
-  of ContentAudio:
-    ctx.pushInlineText(computed, parent, ctx.lctx.audioText)
-  of ContentIFrame:
-    ctx.pushInlineText(computed, parent, ctx.lctx.iframeText)
   of ContentNewline:
     ctx.pushInline(InlineBox(
       t: ibtNewline,
@@ -3446,9 +3432,6 @@ proc layout*(root: StyledNode; attrsp: ptr WindowAttributes): BlockBox =
     positioned: @[PositionedItem(), PositionedItem()],
     myRootProperties: rootProperties(),
     imgText: newCharacterData("[img]"),
-    videoText: newCharacterData("[video]"),
-    audioText: newCharacterData("[audio]"),
-    iframeText: newCharacterData("[iframe]"),
     luctx: LUContext()
   )
   let box = BlockBox(computed: root.computed, node: root.element)
diff --git a/src/css/match.nim b/src/css/match.nim
index c23ac8da..737dd1d6 100644
--- a/src/css/match.nim
+++ b/src/css/match.nim
@@ -216,7 +216,7 @@ func matches*(element: Element; cxsel: ComplexSelector;
     depends: var DependencyInfo): bool =
   var e = element
   var pmatch = mtTrue
-  var mdepends = DependencyInfo()
+  var mdepends = DependencyInfo.default
   for i in countdown(cxsel.high, 0):
     var match = mtFalse
     case cxsel[i].ct
diff --git a/src/css/selectorparser.nim b/src/css/selectorparser.nim
index 026fe140..70da9119 100644
--- a/src/css/selectorparser.nim
+++ b/src/css/selectorparser.nim
@@ -13,16 +13,6 @@ type
     peNone = "-cha-none"
     peBefore = "before"
     peAfter = "after"
-    # internal
-    peInputText = "-cha-input-text"
-    peTextareaText = "-cha-textarea-text"
-    peImage = "-cha-image"
-    peNewline = "-cha-newline"
-    peVideo = "-cha-video"
-    peAudio = "-cha-audio"
-    peCanvas = "-cha-canvas"
-    peSVG = "-cha-svg"
-    peIFrame = "-cha-iframe"
 
   PseudoClass* = enum
     pcFirstChild = "first-child"
diff --git a/src/css/stylednode.nim b/src/css/stylednode.nim
deleted file mode 100644
index 5a123d9f..00000000
--- a/src/css/stylednode.nim
+++ /dev/null
@@ -1,72 +0,0 @@
-import css/cssvalues
-import css/selectorparser
-import html/dom
-
-# Container to hold a style and a node.
-#TODO: maintaining a separate tree for styles is inefficient, and at
-# this point, most of it has been moved to the DOM anyway.
-#
-# The only purpose it has left is to arrange text, element and
-# pseudo-element nodes into a single seq, but this should be done
-# with an iterator instead.
-
-type
-  StyledType* = enum
-    stElement, stText, stReplacement
-
-  StyledNode* = ref object
-    element*: Element
-    pseudo*: PseudoElement
-    case t*: StyledType
-    of stText:
-      text*: CharacterData
-    of stElement:
-      children*: seq[StyledNode]
-    of stReplacement:
-      # replaced elements: quotes, or (TODO) markers, images
-      content*: CSSContent
-
-when defined(debug):
-  func `$`*(node: StyledNode): string =
-    if node == nil:
-      return "nil"
-    case node.t
-    of stText:
-      return "#text " & node.text.data
-    of stElement:
-      if node.pseudo != peNone:
-        return $node.element.tagType & "::" & $node.pseudo
-      return $node.element
-    of stReplacement:
-      return "#replacement"
-
-template computed*(styledNode: StyledNode): CSSValues =
-  styledNode.element.computedMap[styledNode.pseudo]
-
-proc isValid*(styledNode: StyledNode; toReset: var seq[Element]): bool =
-  if styledNode.t in {stText, stReplacement} or styledNode.pseudo != peNone:
-    # pseudo elements do not have selector dependencies
-    return true
-  return styledNode.element.isValid(toReset)
-
-func newStyledElement*(element: Element): StyledNode =
-  return StyledNode(t: stElement, element: element)
-
-func newStyledElement*(parent: StyledNode; pseudo: PseudoElement): StyledNode =
-  return StyledNode(
-    t: stElement,
-    pseudo: pseudo,
-    element: parent.element
-  )
-
-func newStyledText*(parent: StyledNode; text: Text): StyledNode =
-  return StyledNode(t: stText, text: text, element: parent.element)
-
-func newStyledReplacement*(parent: StyledNode; content: sink CSSContent;
-    pseudo: PseudoElement): StyledNode =
-  return StyledNode(
-    t: stReplacement,
-    element: parent.element,
-    content: content,
-    pseudo: pseudo
-  )
diff --git a/src/html/chadombuilder.nim b/src/html/chadombuilder.nim
index 2359e65e..676ae0d8 100644
--- a/src/html/chadombuilder.nim
+++ b/src/html/chadombuilder.nim
@@ -88,6 +88,7 @@ proc restart*(wrapper: HTML5ParserWrapper; charset: Charset) =
 proc setQuirksModeImpl(builder: ChaDOMBuilder; quirksMode: QuirksMode) =
   if not builder.document.parserCannotChangeModeFlag:
     builder.document.mode = quirksMode
+    builder.document.applyQuirksSheet()
 
 proc setEncodingImpl(builder: ChaDOMBuilder; encoding: string):
     SetEncodingResult =
@@ -162,7 +163,7 @@ proc insertTextImpl(builder: ChaDOMBuilder; parent: Node; text: string;
   if prevSibling != nil and prevSibling of Text:
     Text(prevSibling).data &= text
     if parent of Element:
-      Element(parent).invalid = true
+      Element(parent).invalidate()
   else:
     let text = builder.document.createTextNode(text)
     discard parent.insertBefore(text, before)
@@ -209,6 +210,8 @@ proc elementPoppedImpl(builder: ChaDOMBuilder; element: Node) =
     if window != nil:
       let svg = SVGSVGElement(element)
       window.loadResource(svg)
+  elif element of HTMLStyleElement:
+    HTMLStyleElement(element).updateSheet()
 
 proc newChaDOMBuilder(url: URL; window: Window; factory: CAtomFactory;
     confidence: CharsetConfidence; charset = DefaultCharset): ChaDOMBuilder =
diff --git a/src/html/dom.nim b/src/html/dom.nim
index 92011371..167d4b74 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -1,5 +1,6 @@
 import std/algorithm
 import std/deques
+import std/hashes
 import std/math
 import std/options
 import std/posix
@@ -72,12 +73,11 @@ type
   DependencyType* = enum
     dtHover, dtChecked, dtFocus, dtTarget
 
-  DependencyInfoItem = object
-    t: DependencyType
-    element: Element
+  DependencyMap = object
+    dependsOn: Table[Element, seq[Element]]
+    dependedBy: Table[Element, seq[Element]]
 
-  DependencyInfo* = object
-    items: seq[DependencyInfoItem]
+  DependencyInfo* = array[DependencyType, seq[Element]]
 
   Location = ref object
     window: Window
@@ -123,7 +123,7 @@ type
     userAgent*: string
     referrer* {.jsget.}: string
     autofocus*: bool
-    maybeRestyle*: proc()
+    maybeRestyle*: proc(element: Element)
     performance* {.jsget.}: Performance
 
   # Navigator stuff
@@ -213,6 +213,7 @@ type
     throwOnDynamicMarkupInsertion: int
     activeParserWasAborted: bool
     writeBuffers*: seq[DocumentWriteBuffer]
+    styleDependencies: array[DependencyType, DependencyMap]
 
     scriptsToExecSoon*: seq[HTMLScriptElement]
     scriptsToExecInOrder*: Deque[HTMLScriptElement]
@@ -229,8 +230,10 @@ type
     invalid*: bool # whether the document must be rendered again
 
     cachedAll: HTMLAllCollection
-    cachedSheets: seq[CSSStylesheet]
-    cachedSheetsInvalid*: bool
+
+    uaSheets*: seq[CSSStylesheet]
+    userSheet*: CSSStylesheet
+    authorSheets*: seq[CSSStylesheet]
     cachedForms: HTMLCollection
     parser*: RootRef
 
@@ -269,10 +272,7 @@ type
     namespaceURI {.jsget.}: CAtom
     prefix {.jsget.}: CAtom
     internalHover: bool
-    invalid*: bool
-    # The owner StyledNode is marked as invalid when one of these no longer
-    # matches the DOM value.
-    invalidDeps*: set[DependencyType]
+    selfDepends: set[DependencyType]
     localName* {.jsget.}: CAtom
     id* {.jsget.}: CAtom
     name {.jsget.}: CAtom
@@ -282,8 +282,6 @@ type
     cachedAttributes: NamedNodeMap
     cachedStyle*: CSSStyleDeclaration
     computedMap*: array[peNone..peAfter, CSSValues]
-    # All elements our style depends on, for each dependency type d.
-    depends*: DependencyInfo
 
   AttrDummyElement = ref object of Element
 
@@ -343,7 +341,7 @@ type
     value* {.jsget.}: Option[int32]
 
   HTMLStyleElement* = ref object of HTMLElement
-    sheet: CSSStylesheet
+    sheet*: CSSStylesheet
 
   HTMLLinkElement* = ref object of HTMLElement
     sheets: seq[CSSStylesheet]
@@ -540,8 +538,8 @@ proc newHTMLElement*(document: Document; tagType: TagType): HTMLElement
 proc parseColor(element: Element; s: string): ARGBColor
 proc reflectAttr(element: Element; name: CAtom; value: Option[string])
 proc remove*(node: Node)
-proc setInvalid*(element: Element)
-proc setInvalid*(element: Element; dep: DependencyType)
+proc invalidate*(element: Element)
+proc invalidate*(element: Element; dep: DependencyType)
 
 # Forward declaration hacks
 # set in css/match
@@ -2628,6 +2626,9 @@ func serializeFragment*(node: Node): string =
   result.serializeFragment(node)
 
 # Element
+proc hash(element: Element): Hash =
+  return hash(cast[pointer](element))
+
 func innerHTML(element: Element): string {.jsfget.} =
   #TODO xml
   return element.serializeFragment()
@@ -2658,22 +2659,50 @@ func crossOrigin(element: HTMLImageElement): CORSAttribute {.jsfget.} =
 func referrerpolicy(element: HTMLScriptElement): Option[ReferrerPolicy] =
   return strictParseEnum[ReferrerPolicy](element.attr(satReferrerpolicy))
 
-proc sheets*(document: Document): seq[CSSStylesheet] =
-  if document.cachedSheetsInvalid and document.window.styling:
-    document.cachedSheets.setLen(0)
+func applyMediaQuery(ss: CSSStylesheet; window: Window): CSSStylesheet =
+  if ss == nil:
+    return nil
+  var res = CSSStylesheet()
+  res[] = ss[]
+  for mq in ss.mqList:
+    if mq.query.applies(window.settings.scripting, window.attrsp):
+      res.add(mq.children.applyMediaQuery(window))
+  return move(res)
+
+proc applyUASheet*(document: Document) =
+  const ua = staticRead"res/ua.css"
+  document.uaSheets.add(ua.parseStylesheet(document.factory, nil,
+    document.window.attrsp).applyMediaQuery(document.window))
+  if document.documentElement != nil:
+    document.documentElement.invalidate()
+
+proc applyQuirksSheet*(document: Document) =
+  const quirks = staticRead"res/quirk.css"
+  document.uaSheets.add(quirks.parseStylesheet(document.factory, nil,
+    document.window.attrsp).applyMediaQuery(document.window))
+  if document.documentElement != nil:
+    document.documentElement.invalidate()
+
+proc applyUserSheet*(document: Document; user: string) =
+  document.userSheet = user.parseStylesheet(document.factory, nil,
+    document.window.attrsp).applyMediaQuery(document.window)
+  if document.documentElement != nil:
+    document.documentElement.invalidate()
+
+#TODO this should be cached & called incrementally
+proc applyAuthorSheets*(document: Document) =
+  let window = document.window
+  if window.styling and document.documentElement != nil:
+    document.authorSheets = @[]
     for elem in document.documentElement.descendants:
       if elem of HTMLStyleElement:
         let style = HTMLStyleElement(elem)
-        style.sheet = style.textContent.parseStylesheet(document.factory,
-          document.baseURL, document.window.attrsp)
-        document.cachedSheets.add(style.sheet)
+        document.authorSheets.add(style.sheet)
       elif elem of HTMLLinkElement:
         let link = HTMLLinkElement(elem)
         if link.enabled.get(not link.relList.containsIgnoreCase(satAlternate)):
-          document.cachedSheets.add(link.sheets)
-      else: discard
-    document.cachedSheetsInvalid = false
-  return document.cachedSheets
+          document.authorSheets.add(link.sheets)
+    document.documentElement.invalidate()
 
 func isButton*(element: Element): bool =
   if element of HTMLButtonElement:
@@ -2755,10 +2784,10 @@ func focus*(document: Document): Element =
 
 proc setFocus*(document: Document; element: Element) =
   if document.focus != nil:
-    document.focus.setInvalid(dtFocus)
+    document.focus.invalidate(dtFocus)
   document.internalFocus = element
   if element != nil:
-    element.setInvalid(dtFocus)
+    element.invalidate(dtFocus)
 
 proc focus(ctx: JSContext; element: Element) {.jsfunc.} =
   let window = ctx.getWindow()
@@ -2773,16 +2802,16 @@ func target*(document: Document): Element =
 
 proc setTarget*(document: Document; element: Element) =
   if document.target != nil:
-    document.target.setInvalid(dtTarget)
+    document.target.invalidate(dtTarget)
   document.internalTarget = element
   if element != nil:
-    element.setInvalid(dtTarget)
+    element.invalidate(dtTarget)
 
 func hover*(element: Element): bool =
   return element.internalHover
 
 proc setHover*(element: Element; hover: bool) =
-  element.setInvalid(dtHover)
+  element.invalidate(dtHover)
   element.internalHover = hover
 
 func findAutoFocus*(document: Document): Element =
@@ -2969,9 +2998,9 @@ func length(this: HTMLFormElement): int {.jsfget.} =
 func jsForm(this: HTMLInputElement): HTMLFormElement {.jsfget: "form".} =
   return this.form
 
-proc setValue(this: HTMLInputElement; value: string) {.jsfset: "value".} =
+proc setValue*(this: HTMLInputElement; value: string) {.jsfset: "value".} =
   this.value = value
-  this.setInvalid()
+  this.invalidate()
 
 proc setType(this: HTMLInputElement; s: string) {.jsfset: "type".} =
   this.attr(satType, s)
@@ -2984,11 +3013,11 @@ proc setChecked*(input: HTMLInputElement; b: bool) {.jsfset: "checked".} =
   # fully invalidate them on checked change.
   if input.inputType == itRadio:
     for radio in input.radiogroup:
-      radio.setInvalid(dtChecked)
-      radio.setInvalid()
+      radio.invalidate(dtChecked)
+      radio.invalidate()
       radio.internalChecked = false
-  input.setInvalid(dtChecked)
-  input.setInvalid()
+  input.invalidate(dtChecked)
+  input.invalidate()
   input.internalChecked = b
 
 func inputString*(input: HTMLInputElement): string =
@@ -3076,7 +3105,7 @@ func select*(option: HTMLOptionElement): HTMLSelectElement =
 
 proc setSelected*(option: HTMLOptionElement; selected: bool)
     {.jsfset: "selected".} =
-  option.setInvalid(dtChecked)
+  option.invalidate(dtChecked)
   option.selected = selected
   let select = option.select
   if select != nil and not select.attrb(satMultiple):
@@ -3088,12 +3117,12 @@ proc setSelected*(option: HTMLOptionElement; selected: bool)
       if option.selected:
         if prevSelected != nil:
           prevSelected.selected = false
-          prevSelected.setInvalid(dtChecked)
+          prevSelected.invalidate(dtChecked)
         prevSelected = option
     if select.attrul(satSize).get(1) == 1 and
         prevSelected == nil and firstOption != nil:
       firstOption.selected = true
-      firstOption.setInvalid(dtChecked)
+      firstOption.invalidate(dtChecked)
 
 # <select>
 func jsForm(this: HTMLSelectElement): HTMLFormElement {.jsfget: "form".} =
@@ -3229,7 +3258,7 @@ proc setSelectedIndex*(this: HTMLSelectElement; n: int)
       it.dirty = true
     else:
       it.selected = false
-    it.setInvalid(dtChecked)
+    it.invalidate(dtChecked)
     it.invalidateCollections()
     inc i
 
@@ -3271,6 +3300,15 @@ proc remove(ctx: JSContext; this: HTMLSelectElement; idx: varargs[JSValue]):
     this.remove()
   ok()
 
+# <style>
+proc updateSheet*(this: HTMLStyleElement) =
+  let document = this.document
+  let window = document.window
+  if window != nil:
+    this.sheet = this.textContent.parseStylesheet(document.factory,
+      document.baseURL, window.attrsp).applyMediaQuery(window)
+    document.applyAuthorSheets()
+
 # <table>
 func caption(this: HTMLTableElement): Element {.jsfget.} =
   return this.findFirstChildOf(TAG_CAPTION)
@@ -3750,15 +3788,27 @@ proc delAttr(element: Element; i: int; keep = false) =
       map.attrlist.del(j) # ordering does not matter
   element.reflectAttr(name, none(string))
   element.invalidateCollections()
-  element.setInvalid()
+  element.invalidate()
 
 # Styles.
-#
+template computed*(element: Element): CSSValues =
+  element.computedMap[peNone]
+
+proc invalidate*(element: Element) =
+  let valid = element.computed != nil
+  for it in element.computedMap.mitems:
+    it = nil
+  if element.document != nil:
+    element.document.invalid = true
+  if valid:
+    for it in element.elementList:
+      it.invalidate()
+
 # 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.
+# each element holds a list of elements their CSS values depend on.
+# (This list may include the element itself.) In addition, elements
+# 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:
@@ -3783,36 +3833,37 @@ proc delAttr(element: Element; i: int; keep = false) =
 # 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.
-proc setInvalid*(element: Element) =
-  element.invalid = true
-  if element.document != nil:
-    element.document.invalid = true
 
-proc setInvalid*(element: Element; dep: DependencyType) =
-  element.invalidDeps.incl(dep)
-  if element.document != nil:
-    element.document.invalid = true
-
-template computed*(element: Element): CSSValues =
-  element.computedMap[peNone]
+proc invalidate*(element: Element; dep: DependencyType) =
+  if dep in element.selfDepends:
+    element.invalidate()
+  element.document.styleDependencies[dep].dependedBy.withValue(element, p):
+    for it in p[]:
+      it.invalidate()
 
-proc isValid*(element: Element; toReset: var seq[Element]): bool =
-  if element.invalid:
-    toReset.add(element)
-    return false
-  for it in element.depends.items:
-    if it.t in it.element.invalidDeps:
-      toReset.add(it.element)
-      return false
-  return true
+proc applyStyleDependencies*(element: Element; depends: DependencyInfo) =
+  let document = element.document
+  element.selfDepends = {}
+  for t, map in document.styleDependencies.mpairs:
+    map.dependsOn.withValue(element, p):
+      for it in p[]:
+        map.dependedBy.del(it)
+      document.styleDependencies[t].dependsOn.del(element)
+    for el in depends[t]:
+      if el == element:
+        element.selfDepends.incl(t)
+        continue
+      document.styleDependencies[t].dependedBy.mgetOrPut(el, @[]).add(element)
+      document.styleDependencies[t].dependsOn.mgetOrPut(element, @[]).add(el)
 
 proc add*(depends: var DependencyInfo; element: Element; t: DependencyType) =
-  depends.items.add(DependencyInfoItem(t: t, element: element))
+  depends[t].add(element)
 
 proc merge*(a: var DependencyInfo; b: DependencyInfo) =
-  for it in b.items:
-    if it notin a.items:
-      a.items.add(it)
+  for t, it in b:
+    for x in it:
+      if x notin a[t]:
+        a[t].add(x)
 
 proc newCSSStyleDeclaration(element: Element; value: string; computed = false;
     readonly = false): CSSStyleDeclaration =
@@ -3959,7 +4010,7 @@ proc getComputedStyle0*(window: Window; element: Element;
   of "": peNone
   else: return newCSSStyleDeclaration(nil, "")
   if window.settings.scripting == smApp:
-    window.maybeRestyle()
+    window.maybeRestyle(element)
     return newCSSStyleDeclaration(element, $element.computedMap[pseudo],
       computed = true, readonly = true)
   # In lite mode, we just parse the "style" attribute and hope for
@@ -3989,8 +4040,10 @@ proc loadSheet(window: Window; link: HTMLLinkElement; url: URL; applies: bool) =
       if applies:
         # Note: we intentionally load all sheets to prevent media query
         # based tracking.
-        link.sheets.add(sheet)
-        window.document.cachedSheetsInvalid = true
+        link.sheets.add(sheet.applyMediaQuery(window))
+        window.document.applyAuthorSheets()
+        if window.document.documentElement != nil:
+          window.document.documentElement.invalidate()
       for url in sheet.importList:
         window.loadSheet(link, url, true) #TODO media query
   )
@@ -4024,8 +4077,9 @@ proc getImageId(window: Window): int =
 
 proc loadResource*(window: Window; image: HTMLImageElement) =
   if not window.images:
-    image.invalid = image.invalid or image.bitmap != nil
-    image.bitmap = nil
+    if image.bitmap != nil:
+      image.invalidate()
+      image.bitmap = nil
     image.fetchStarted = false
     return
   if image.fetchStarted:
@@ -4124,16 +4178,17 @@ proc loadResource*(window: Window; image: HTMLImageElement) =
           cachedURL.bmp = bmp
           for share in cachedURL.shared:
             share.bitmap = bmp
-            share.setInvalid()
-          image.setInvalid()
+            share.invalidate()
+          image.invalidate()
         )
       )
     window.pendingResources.add(p)
 
 proc loadResource*(window: Window; svg: SVGSVGElement) =
   if not window.images:
-    svg.invalid = svg.invalid or svg.bitmap != nil
-    svg.bitmap = nil
+    if svg.bitmap != nil:
+      svg.invalidate()
+      svg.bitmap = nil
     svg.fetchStarted = false
     return
   if svg.fetchStarted:
@@ -4144,7 +4199,7 @@ proc loadResource*(window: Window; svg: SVGSVGElement) =
     window.svgCache.withValue(s, elp):
       svg.bitmap = elp.bitmap
       if svg.bitmap != nil: # already decoded
-        svg.setInvalid()
+        svg.invalidate()
       else: # tell me when you're done
         elp.shared.add(svg)
       return
@@ -4197,8 +4252,8 @@ proc loadResource*(window: Window; svg: SVGSVGElement) =
     )
     for share in svg.shared:
       share.bitmap = svg.bitmap
-      share.setInvalid()
-    svg.setInvalid()
+      share.invalidate()
+    svg.invalidate()
   )
   window.pendingResources.add(p)
 
@@ -4291,7 +4346,7 @@ proc reflectAttr(element: Element; name: CAtom; value: Option[string]) =
     if name == satDisabled:
       # IE won :(
       if link.enabled.isNone:
-        link.document.cachedSheetsInvalid = true
+        link.document.applyAuthorSheets()
       link.enabled = some(value.isNone)
     if link.isConnected and name in {satHref, satRel, satDisabled}:
       link.fetchStarted = false
@@ -4354,7 +4409,7 @@ proc attr*(element: Element; name: CAtom; value: string) =
   if i >= 0:
     element.attrs[i].value = value
     element.invalidateCollections()
-    element.setInvalid()
+    element.invalidate()
   else:
     element.attrs.insert(AttrData(
       qualifiedName: name,
@@ -4385,7 +4440,7 @@ proc attrns*(element: Element; localName: CAtom; prefix: NamespacePrefix;
     element.attrs[i].qualifiedName = qualifiedName
     element.attrs[i].value = value
     element.invalidateCollections()
-    element.setInvalid()
+    element.invalidate()
   else:
     element.attrs.insert(AttrData(
       prefix: prefixAtom,
@@ -4536,14 +4591,15 @@ proc remove*(node: Node; suppressObservers: bool) =
   parent.invalidateCollections()
   node.invalidateCollections()
   if parent of Element:
-    Element(parent).setInvalid()
+    Element(parent).invalidate()
   node.parentNode = nil
   node.index = -1
   if element != nil:
     element.elIndex = -1
-    if element.document != nil and
-        (element of HTMLStyleElement or element of HTMLLinkElement):
-      element.document.cachedSheetsInvalid = true
+    if element.document != nil:
+      if element of HTMLStyleElement or element of HTMLLinkElement:
+        element.document.applyAuthorSheets()
+      element.applyStyleDependencies(DependencyInfo.default)
   #TODO assigned, shadow root, shadow root again, custom nodes, registered
   # observers
   #TODO not suppress observers => queue tree mutation record
@@ -4579,7 +4635,7 @@ proc resetElement*(element: Element) =
       input.files.setLen(0)
     else:
       input.value = input.attr(satValue)
-    input.setInvalid()
+    input.invalidate()
   of TAG_SELECT:
     let select = HTMLSelectElement(element)
     var firstOption: HTMLOptionElement = nil
@@ -4592,7 +4648,7 @@ proc resetElement*(element: Element) =
       if option.selected:
         if not multiple and prevSelected != nil:
           prevSelected.selected = false
-          prevSelected.setInvalid(dtChecked)
+          prevSelected.invalidate(dtChecked)
         prevSelected = option
     if not multiple and select.attrul(satSize).get(1) == 1 and
         prevSelected == nil and firstOption != nil:
@@ -4600,7 +4656,7 @@ proc resetElement*(element: Element) =
   of TAG_TEXTAREA:
     let textarea = HTMLTextAreaElement(element)
     textarea.value = textarea.childTextContent()
-    textarea.setInvalid()
+    textarea.invalidate()
   else: discard
 
 proc setForm*(element: FormAssociatedElement; form: HTMLFormElement) =
@@ -4645,7 +4701,8 @@ proc resetFormOwner(element: FormAssociatedElement) =
         element.setForm(HTMLFormElement(ancestor))
 
 proc elementInsertionSteps(element: Element) =
-  if element of HTMLOptionElement:
+  case element.tagType
+  of TAG_OPTION:
     if element.parentElement != nil:
       let parent = element.parentElement
       var select: HTMLSelectElement
@@ -4656,21 +4713,23 @@ proc elementInsertionSteps(element: Element) =
         select = HTMLSelectElement(parent.parentElement)
       if select != nil:
         select.resetElement()
-  elif element of FormAssociatedElement:
-    let element = FormAssociatedElement(element)
-    if element.parserInserted:
-      return
-    element.resetFormOwner()
-  elif element of HTMLLinkElement:
+  of TAG_LINK:
     let window = element.document.window
     if window != nil:
       let link = HTMLLinkElement(element)
       window.loadResource(link)
-  elif element of HTMLImageElement:
+  of TAG_IMAGE:
     let window = element.document.window
     if window != nil:
       let image = HTMLImageElement(element)
       window.loadResource(image)
+  of TAG_STYLE:
+    HTMLStyleElement(element).updateSheet()
+  elif element of FormAssociatedElement:
+    let element = FormAssociatedElement(element)
+    if element.parserInserted:
+      return
+    element.resetFormOwner()
 
 func isValidParent(node: Node): bool =
   return node of Element or node of Document or node of DocumentFragment
@@ -4761,7 +4820,7 @@ proc insertNode(parent, node, before: Node) =
   parent.invalidateCollections()
   if node.document != nil and (node of HTMLStyleElement or
       node of HTMLLinkElement):
-    node.document.cachedSheetsInvalid = true
+    node.document.applyAuthorSheets()
   for el in node.elementsIncl:
     #TODO shadow root
     el.elementInsertionSteps()
@@ -4783,7 +4842,7 @@ proc insert*(parent, node, before: Node; suppressObservers = false) =
     #TODO live ranges
     discard
   if parent of Element:
-    Element(parent).setInvalid()
+    Element(parent).invalidate()
   for node in nodes:
     insertNode(parent, node, before)
 
@@ -4986,7 +5045,7 @@ proc setTextContent(ctx: JSContext; node: Node; data: JSValue): Err[void]
 proc reset*(form: HTMLFormElement) =
   for control in form.controls:
     control.resetElement()
-    control.setInvalid()
+    control.invalidate()
 
 proc renderBlocking(element: Element): bool =
   if "render" in element.attr(satBlocking).split(AsciiWhitespace):
diff --git a/src/server/buffer.nim b/src/server/buffer.nim
index 9258b293..ce44ac56 100644
--- a/src/server/buffer.nim
+++ b/src/server/buffer.nim
@@ -17,7 +17,6 @@ import css/layout
 import css/lunit
 import css/render
 import css/sheet
-import css/stylednode
 import html/catom
 import html/chadombuilder
 import html/dom
@@ -94,11 +93,9 @@ type
     lines: FlexibleGrid
     loader: FileLoader
     needsBOMSniff: bool
-    needsReshape: bool
     outputId: int
     pollData: PollData
     prevHover: Element
-    prevStyled: StyledNode
     pstream: SocketStream # control stream
     quirkstyle: CSSStylesheet
     reportedBytesRead: int
@@ -107,9 +104,7 @@ type
     savetask: bool
     state: BufferState
     tasks: array[BufferCommand, int] #TODO this should have arguments
-    uastyle: CSSStylesheet
     url: URL # URL before readFromFd
-    userstyle: CSSStylesheet
     window: Window
 
   BufferIfaceItem = object
@@ -766,34 +761,15 @@ proc checkRefresh*(buffer: Buffer): CheckRefreshResult {.proxy.} =
     return CheckRefreshResult(n: -1)
   return CheckRefreshResult(n: n, url: url.get)
 
-proc maybeRestyle(buffer: Buffer) =
-  if buffer.document == nil:
-    return
-  if buffer.document.invalid or buffer.document.cachedSheetsInvalid:
-    let uastyle = if buffer.document.mode != QUIRKS:
-      buffer.uastyle
-    else:
-      buffer.quirkstyle
-    if buffer.document.cachedSheetsInvalid:
-      buffer.prevStyled = nil
-    let styledRoot = buffer.document.applyStylesheets(uastyle,
-      buffer.userstyle, buffer.prevStyled)
-    buffer.prevStyled = styledRoot
-    buffer.document.invalid = false
-    buffer.needsReshape = true
-
 proc maybeReshape(buffer: Buffer): bool {.discardable.} =
-  if buffer.document == nil:
+  if buffer.document == nil and buffer.document.documentElement != nil:
     return # not parsed yet, nothing to render
-  buffer.maybeRestyle()
-  if buffer.needsReshape:
-    buffer.rootBox = nil
-    # applyStylesheets may return nil if there is no <html> element.
-    if buffer.prevStyled != nil:
-      buffer.rootBox = buffer.prevStyled.layout(addr buffer.attrs)
+  if buffer.document.invalid:
+    let root = initStyledElement(buffer.document.documentElement)
+    buffer.rootBox = root.layout(addr buffer.attrs)
     buffer.lines.renderDocument(buffer.bgcolor, buffer.rootBox,
       addr buffer.attrs, buffer.images)
-    buffer.needsReshape = false
+    buffer.document.invalid = false
     return true
   return false
 
@@ -814,7 +790,8 @@ proc processData0(buffer: Buffer; data: UnsafeSlice): bool =
         Text(lastChild).data &= data
       else:
         plaintext.insert(buffer.document.createTextNode($data), nil)
-      plaintext.setInvalid()
+      #TODO just invalidate document?
+      plaintext.invalidate()
   true
 
 func canSwitch(buffer: Buffer): bool {.inline.} =
@@ -831,7 +808,9 @@ proc switchCharset(buffer: Buffer) =
   buffer.initDecoder()
   buffer.htmlParser.restart(buffer.charset)
   buffer.document = buffer.htmlParser.builder.document
-  buffer.prevStyled = nil
+  buffer.document.applyUASheet()
+  buffer.document.applyUserSheet(buffer.config.userstyle)
+  buffer.document.invalid = true
 
 proc bomSniff(buffer: Buffer; iq: openArray[uint8]): int =
   if iq[0] == 0xFE and iq[1] == 0xFF:
@@ -1125,6 +1104,7 @@ proc onload(buffer: Buffer) =
       reprocess = false
     else: # EOF
       buffer.finishLoad().then(proc() =
+        buffer.document.invalid = true
         buffer.maybeReshape()
         buffer.state = bsLoaded
         buffer.document.readyState = rsComplete
@@ -1164,11 +1144,15 @@ proc getTitle*(buffer: Buffer): string {.proxy, task.} =
 proc forceReshape0(buffer: Buffer) =
   if buffer.document != nil:
     buffer.document.invalid = true
-  buffer.needsReshape = true
   buffer.maybeReshape()
 
 proc forceReshape2(buffer: Buffer) =
-  buffer.prevStyled = nil
+  if buffer.document != nil and buffer.document.documentElement != nil:
+    buffer.document.documentElement.invalidate()
+  buffer.document.applyUASheet()
+  if buffer.document.mode == QUIRKS:
+    buffer.document.applyQuirksSheet()
+  buffer.document.applyUserSheet(buffer.config.userstyle)
   buffer.forceReshape0()
 
 proc forceReshape*(buffer: Buffer) {.proxy.} =
@@ -1370,20 +1354,19 @@ proc readSuccess*(buffer: Buffer; s: string; hasFd: bool): ReadSuccessResult
       case input.inputType
       of itFile:
         input.files = @[newWebFile(s, fd)]
-        input.setInvalid()
+        input.invalidate()
         buffer.maybeReshape()
         res.repaint = true
         res.open = buffer.implicitSubmit(input)
       else:
-        input.value = s
-        input.setInvalid()
+        input.setValue(s)
         buffer.maybeReshape()
         res.repaint = true
         res.open = buffer.implicitSubmit(input)
     of TAG_TEXTAREA:
       let textarea = HTMLTextAreaElement(buffer.document.focus)
       textarea.value = s
-      textarea.setInvalid()
+      textarea.invalidate()
       buffer.maybeReshape()
       res.repaint = true
     else: discard
@@ -1959,7 +1942,9 @@ proc launchBuffer*(config: BufferConfig; url: URL; attrs: WindowAttributes;
   if buffer.config.scripting != smFalse:
     buffer.window.navigate = proc(url: URL) = buffer.navigate(url)
     if buffer.config.scripting == smApp:
-      buffer.window.maybeRestyle = proc() = buffer.maybeRestyle()
+      buffer.window.maybeRestyle = proc(element: Element) =
+        if element.computed == nil:
+          element.applyStyle()
   buffer.charset = buffer.charsetStack.pop()
   buffer.fd = istream.fd
   buffer.istream = istream
@@ -1970,14 +1955,7 @@ proc launchBuffer*(config: BufferConfig; url: URL; attrs: WindowAttributes;
   loader.unregisterFun = proc(fd: int) =
     buffer.pollData.unregister(fd)
   buffer.pollData.register(buffer.rfd, POLLIN)
-  const css = staticRead"res/ua.css"
-  const quirk = css & staticRead"res/quirk.css"
   buffer.initDecoder()
-  let attrsp = addr buffer.attrs
-  buffer.uastyle = css.parseStylesheet(factory, nil, attrsp)
-  buffer.quirkstyle = quirk.parseStylesheet(factory, nil, attrsp)
-  buffer.userstyle = buffer.config.userstyle.parseStylesheet(factory, nil,
-    attrsp)
   buffer.htmlParser = newHTML5ParserWrapper(
     buffer.window,
     buffer.url,
@@ -1987,6 +1965,8 @@ proc launchBuffer*(config: BufferConfig; url: URL; attrs: WindowAttributes;
   )
   assert buffer.htmlParser.builder.document != nil
   buffer.document = buffer.htmlParser.builder.document
+  buffer.document.applyUASheet()
+  buffer.document.applyUserSheet(buffer.config.userstyle)
   buffer.runBuffer()
   buffer.cleanup()
   quit(0)