about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2021-11-10 18:26:18 +0100
committerbptato <nincsnevem662@gmail.com>2021-11-10 18:32:25 +0100
commitfcd3a5b204e15fdfc739fd04975977d288e892e0 (patch)
tree363f3bfd60570ce5fb1f4fbc6c62557609ccc6ea
parente6f7cc72ba3343fb81c4f8196446c58eca59191e (diff)
downloadchawan-fcd3a5b204e15fdfc739fd04975977d288e892e0.tar.gz
Layout engine improvements, use author style sheet
-rw-r--r--readme.md7
-rw-r--r--res/default.css2
-rw-r--r--src/css/parser.nim2
-rw-r--r--src/css/style.nim20
-rw-r--r--src/html/dom.nim128
-rw-r--r--src/html/parser.nim14
-rw-r--r--src/io/buffer.nim123
-rw-r--r--src/layout/box.nim15
-rw-r--r--src/layout/layout.nim77
-rw-r--r--src/main.nim2
-rw-r--r--src/types/enums.nim4
-rw-r--r--src/utils/eprint.nim36
12 files changed, 279 insertions, 151 deletions
diff --git a/readme.md b/readme.md
index 3136ea55..9f14f854 100644
--- a/readme.md
+++ b/readme.md
@@ -30,19 +30,20 @@ most important APIs. Plus some other things.
 
 Currently implemented features are:
 
-* basic html rendering (very much WIP)
+* basic html rendering with CSS (very much WIP)
 * fully functioning 2d pager with custom keybindings
 
 Planned features (roughly in order of importance):
 
-* improved html rendering (i.e. actually functioning)
+* refactored and improved layout engine (with colors, inline blocks etc)
 * anchor
 * html generator (for source view)	
-* loading author stylesheets (i.e. ones in web pages)
+* load external resources (e.g. css)
 * markdown (with built-in parser)
 * form (w/ input etc)
 * JavaScript
 * table
+* config editor
 * cookie
 * SOCKS proxy
 * HTTP proxy
diff --git a/res/default.css b/res/default.css
index 5bb5526c..c0a62f90 100644
--- a/res/default.css
+++ b/res/default.css
@@ -10,6 +10,8 @@ menu, noframes, body {
 
 pre {
 	margin-top: 1em;
+	margin-bottom: 1em;
+	white-space: pre;
 }
 
 a, abbr, b, bdo, button, cite, code, del, dfn, em, font, i, img, ins,
diff --git a/src/css/parser.nim b/src/css/parser.nim
index 25a0e21d..08ffeb02 100644
--- a/src/css/parser.nim
+++ b/src/css/parser.nim
@@ -789,6 +789,8 @@ proc printc*(c: CSSComponentValue) =
     else: discard
 
 proc parseCSS*(inputStream: Stream): CSSStylesheet =
+  if inputStream.atEnd():
+    return CSSStylesheet()
   return inputstream.parseStylesheet()
 
 proc debugparseCSS*(inputStream: Stream) =
diff --git a/src/css/style.nim b/src/css/style.nim
index 5f426e58..9db9813e 100644
--- a/src/css/style.nim
+++ b/src/css/style.nim
@@ -29,6 +29,8 @@ type
       display*: DisplayType
     of VALUE_CONTENT:
       content*: seq[Rune]
+    of VALUE_WHITESPACE:
+      whitespace*: WhitespaceType
     of VALUE_NONE: discard
 
   CSSComputedValues* = array[low(CSSRuleType)..high(CSSRuleType), CSSComputedValue]
@@ -48,6 +50,7 @@ const ValueTypes = {
   RULE_FONT_STYLE: VALUE_FONT_STYLE,
   RULE_DISPLAY: VALUE_DISPLAY,
   RULE_CONTENT: VALUE_CONTENT,
+  RULE_WHITESPACE: VALUE_WHITESPACE,
 }.toTable()
 
 func getValueType*(rule: CSSRuleType): CSSValueType =
@@ -237,6 +240,19 @@ func cssFontStyle(d: CSSDeclaration): CSSFontStyle =
       else: return FONTSTYLE_NORMAL
   return FONTSTYLE_NORMAL
 
+func cssWhiteSpace(d: CSSDeclaration): WhitespaceType =
+  if isToken(d):
+    let tok = getToken(d)
+    if tok.tokenType == CSS_IDENT_TOKEN:
+      case $tok.value
+      of "normal": return WHITESPACE_NORMAL
+      of "nowrap": return WHITESPACE_NOWRAP
+      of "pre": return WHITESPACE_PRE
+      of "pre-line": return WHITESPACE_PRE_LINE
+      of "pre-wrap": return WHITESPACE_PRE_WRAP
+      else: return WHITESPACE_NORMAL
+  return WHITESPACE_NORMAL
+
 func getSpecifiedValue*(d: CSSDeclaration): CSSSpecifiedValue =
   case $d.name
   of "color":
@@ -257,6 +273,8 @@ func getSpecifiedValue*(d: CSSDeclaration): CSSSpecifiedValue =
     return CSSSpecifiedValue(t: RULE_DISPLAY, v: VALUE_DISPLAY, display: cssDisplay(d))
   of "content":
     return CSSSpecifiedValue(t: RULE_CONTENT, v: VALUE_CONTENT, content: cssString(d))
+  of "white-space":
+    return CSSSpecifiedValue(t: RULE_WHITESPACE, v: VALUE_WHITESPACE, whitespace: cssWhiteSpace(d))
 
 func getInitialColor*(t: CSSRuleType): CSSColor =
   case t
@@ -292,6 +310,8 @@ func getComputedValue*(rule: CSSSpecifiedValue, parent: CSSValues): CSSComputedV
     return CSSComputedValue(t: rule.t, v: VALUE_FONT_STYLE, fontstyle: rule.fontstyle)
   of VALUE_CONTENT:
     return CSSComputedValue(t: rule.t, v: VALUE_CONTENT, content: rule.content)
+  of VALUE_WHITESPACE:
+    return CSSComputedValue(t: rule.t, v: VALUE_WHITESPACE, whitespace: rule.whitespace)
   of VALUE_NONE: return CSSComputedValue(t: rule.t, v: VALUE_NONE)
 
 func getComputedValue*(d: CSSDeclaration, parent: CSSValues): CSSComputedValue =
diff --git a/src/html/dom.nim b/src/html/dom.nim
index 647b1f7c..92d8cf1b 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -268,6 +268,18 @@ func getAttrValue*(element: Element, s: string): string =
   return ""
 
 #TODO case sensitivity
+
+
+type SelectResult = object
+  success: bool
+  pseudo: PseudoElem
+
+func selectres(s: bool, p: PseudoElem = PSEUDO_NONE): SelectResult =
+  return SelectResult(success: s, pseudo: p)
+
+func psuccess(s: SelectResult): bool =
+  return s.pseudo == PSEUDO_NONE and s.success
+
 func attrSelectorMatches(elem: Element, sel: Selector): bool =
   case sel.rel
   of ' ': return sel.attr in elem.attributes
@@ -287,36 +299,41 @@ func pseudoSelectorMatches(elem: Element, sel: Selector): bool =
   of "last-child": return elem.parentNode.lastElementChild == elem
   else: return false
 
-func pseudoElemSelectorMatches(elem: Element, sel: Selector, pseudo: PseudoElem = PSEUDO_NONE): bool =
+func pseudoElemSelectorMatches(elem: Element, sel: Selector, pseudo: PseudoElem = PSEUDO_NONE): SelectResult =
   case sel.elem
-  of "after": return pseudo == PSEUDO_AFTER
-  of "before": return pseudo == PSEUDO_BEFORE
-  else: return false
+  of "after": return selectres(true, PSEUDO_AFTER)
+  of "before": return selectres(true, PSEUDO_AFTER)
+  else: return selectres(false)
 
-func selectorMatches(elem: Element, sel: Selector, pseudo: PseudoElem = PSEUDO_NONE): bool =
+func selectorMatches(elem: Element, sel: Selector): SelectResult =
   case sel.t
   of TYPE_SELECTOR:
-    return elem.tagType == sel.tag
+    return selectres(elem.tagType == sel.tag)
   of CLASS_SELECTOR:
-    return sel.class in elem.classList
+    return selectres(sel.class in elem.classList)
   of ID_SELECTOR:
-    return sel.id == elem.id
+    return selectres(sel.id == elem.id)
   of ATTR_SELECTOR:
-    return elem.attrSelectorMatches(sel)
+    return selectres(elem.attrSelectorMatches(sel))
   of PSEUDO_SELECTOR:
-    return pseudoSelectorMatches(elem, sel)
+    return selectres(pseudoSelectorMatches(elem, sel))
   of PSELEM_SELECTOR:
-    return pseudoElemSelectorMatches(elem, sel, pseudo)
+    return pseudoElemSelectorMatches(elem, sel)
   of UNIVERSAL_SELECTOR:
-    return true
+    return selectres(true)
   of FUNC_SELECTOR:
-    return false
+    return selectres(false)
 
-func selectorsMatch(elem: Element, selectors: SelectorList, pseudo: PseudoElem = PSEUDO_NONE): bool =
+func selectorsMatch(elem: Element, selectors: SelectorList): SelectResult =
   for sel in selectors.sels:
-    if not selectorMatches(elem, sel, pseudo):
-      return false
-  return true
+    let res = selectorMatches(elem, sel)
+    if not res.success:
+      return selectres(false)
+    if res.pseudo != PSEUDO_NONE:
+      if result.pseudo != PSEUDO_NONE:
+        return selectres(false)
+      result.pseudo = res.pseudo
+  result.success = true
 
 func selectElems(document: Document, sel: Selector): seq[Element] =
   case sel.t
@@ -337,9 +354,9 @@ func selectElems(document: Document, sel: Selector): seq[Element] =
   of FUNC_SELECTOR:
     case sel.name
     of "not":
-      return document.all_elements.filter((elem) => not selectorsMatch(elem, sel.selectors))
+      return document.all_elements.filter((elem) => not selectorsMatch(elem, sel.selectors).psuccess)
     of "is", "where":
-      return document.all_elements.filter((elem) => selectorsMatch(elem, sel.selectors))
+      return document.all_elements.filter((elem) => selectorsMatch(elem, sel.selectors).psuccess)
     return newSeq[Element]()
 
 func selectElems(document: Document, selectors: SelectorList): seq[Element] =
@@ -352,12 +369,12 @@ func selectElems(document: Document, selectors: SelectorList): seq[Element] =
     if sellist[i].t == FUNC_SELECTOR:
       case sellist[i].name
       of "not":
-        result = result.filter((elem) => not selectorsMatch(elem, sellist[i].selectors))
+        result = result.filter((elem) => not selectorsMatch(elem, sellist[i].selectors).psuccess)
       of "is", "where":
-        result = result.filter((elem) => selectorsMatch(elem, sellist[i].selectors))
+        result = result.filter((elem) => selectorsMatch(elem, sellist[i].selectors).psuccess)
       else: discard
     else:
-      result = result.filter((elem) => selectorMatches(elem, sellist[i]))
+      result = result.filter((elem) => selectorMatches(elem, sellist[i]).psuccess)
     inc i
 
 proc querySelector*(document: Document, q: string): seq[Element] =
@@ -389,17 +406,17 @@ proc applyProperty(elem: Element, decl: CSSDeclaration, pseudo: PseudoElem = PSE
     elem.cssvalues_after.get[cval.t] = cval
 
 type ParsedRule = tuple[sels: seq[SelectorList], oblock: CSSSimpleBlock]
+type ParsedStylesheet = seq[ParsedRule]
 
-func calcRules(elem: Element, rules: seq[ParsedRule]):
+func calcRules(elem: Element, rules: ParsedStylesheet):
     array[low(PseudoElem)..high(PseudoElem), seq[CSSSimpleBlock]] =
   var tosorts: array[low(PseudoElem)..high(PseudoElem), seq[tuple[s:int,b:CSSSimpleBlock]]]
   for rule in rules:
     for sel in rule.sels:
-      #TODO: optimize, like rewrite selector match algorithm output or something
-      for pseudo in low(PseudoElem)..high(PseudoElem):
-        if elem.selectorsMatch(sel, pseudo):
-          let spec = getSpecificity(sel)
-          tosorts[pseudo].add((spec,rule.oblock))
+      let match = elem.selectorsMatch(sel)
+      if match.success:
+        let spec = getSpecificity(sel)
+        tosorts[match.pseudo].add((spec,rule.oblock))
 
   for i in low(PseudoElem)..high(PseudoElem):
     tosorts[i].sort((x, y) => cmp(x.s,y.s))
@@ -413,9 +430,6 @@ proc applyRules*(document: Document, rules: CSSStylesheet): seq[tuple[e:Element,
   let parsed = rules.value.map((x) => (sels: parseSelectors(x.prelude), oblock: x.oblock))
   while stack.len > 0:
     let elem = stack.pop()
-    #TODO: optimize
-    #ok this whole idea was stupid, what I should've done is to just check for
-    #pseudo elem selectors, this is way too slow
     let rules_pseudo = calcRules(elem, parsed)
     for pseudo in low(PseudoElem)..high(PseudoElem):
       let rules = rules_pseudo[pseudo]
@@ -429,11 +443,53 @@ proc applyRules*(document: Document, rules: CSSStylesheet): seq[tuple[e:Element,
             else:
               elem.applyProperty(decl, pseudo)
 
-      for child in elem.children:
-        stack.add(child)
+    for child in elem.children:
+      stack.add(child)
+
+proc applyAuthorRules*(document: Document): seq[tuple[e:Element,d:CSSDeclaration]] =
+  var stack: seq[Element]
+  var embedded_rules: seq[ParsedStylesheet]
+
+  stack.add(document.root)
+
+  #let parsed = rules.value.map((x) => (sels: parseSelectors(x.prelude), oblock: x.oblock))
+
+  while stack.len > 0:
+    let elem = stack.pop()
+    var rules_local = ""
+    for child in elem.children:
+      if child.tagType == TAG_STYLE:
+        for ct in child.childNodes:
+          if ct.nodeType == TEXT_NODE:
+            rules_local &= Text(ct).data
+
+    if rules_local.len > 0:
+      let parsed = parseCSS(newStringStream(rules_local)).value.map((x) => (sels: parseSelectors(x.prelude), oblock: x.oblock))
+      embedded_rules.add(parsed)
+    let this_rules = embedded_rules.concat()
+    let rules_pseudo = calcRules(elem, this_rules)
+
+    for pseudo in low(PseudoElem)..high(PseudoElem):
+      let rules = rules_pseudo[pseudo]
+      for rule in rules:
+        let decls = parseCSSListOfDeclarations(rule.value)
+        for item in decls:
+          if item of CSSDeclaration:
+            let decl = CSSDeclaration(item)
+            if decl.important:
+              result.add((elem, decl))
+            else:
+              elem.applyProperty(decl, pseudo)
+
+    for child in elem.children:
+      stack.add(child)
+
+    if rules_local.len > 0:
+      discard embedded_rules.pop()
 
-proc applyDefaultStylesheet*(document: Document) =
-  let important = document.applyRules(stylesheet)
-  for rule in important:
+proc applyStylesheets*(document: Document) =
+  let important_ua = document.applyRules(stylesheet)
+  let important_author = document.applyAuthorRules()
+  for rule in important_ua:
     rule.e.applyProperty(rule.d)
 
diff --git a/src/html/parser.nim b/src/html/parser.nim
index 536ba434..eed5baa7 100644
--- a/src/html/parser.nim
+++ b/src/html/parser.nim
@@ -202,12 +202,21 @@ proc processDocumentBody(state: var HTMLParseState) =
       state.elementNode = state.elementNode.ownerDocument.body
 
 proc processDocumentAddNode(state: var HTMLParseState, newNode: Node) =
-  if state.elementNode.nodeType == ELEMENT_NODE and state.elementNode.tagType == TAG_HTML:
+  if state.elementNode.tagType == TAG_HTML:
     if state.in_body:
       state.elementNode = state.elementNode.ownerDocument.body
     else:
       state.elementNode = state.elementNode.ownerDocument.head
 
+  #> If the next token is a U+000A LINE FEED (LF) character token, then ignore
+  #> that token and move on to the next one. (Newlines at the start of pre
+  #> blocks are ignored as an authoring convenience.)
+  elif state.elementNode.tagType == TAG_PRE:
+    if state.elementNode.childNodes.len == 1 and
+        state.elementNode.childNodes[0].nodeType == TEXT_NODE and
+        Text(state.elementNode.childNodes[0]).data == "\n":
+      discard state.elementNode.childNodes.pop()
+
   insertNode(state.elementNode, newNode)
 
 proc processDocumentEndNode(state: var HTMLParseState) =
@@ -220,7 +229,6 @@ proc processDocumentText(state: var HTMLParseState) =
     processDocumentBody(state)
   if state.textNode == nil:
     state.textNode = newText()
-
     processDocumentAddNode(state, state.textNode)
 
 proc processDocumentStartElement(state: var HTMLParseState, element: Element, tag: DOMParsedTag) =
@@ -426,7 +434,7 @@ proc parseHtml*(inputStream: Stream): Document =
   var buf = ""
   var lineBuf: string
   while not inputStream.atEnd():
-    lineBuf = inputStream.readLine()
+    lineBuf = inputStream.readLine() & '\n'
     buf &= lineBuf
 
     var at = 0
diff --git a/src/io/buffer.nim b/src/io/buffer.nim
index a3dde1b7..9775a267 100644
--- a/src/io/buffer.nim
+++ b/src/io/buffer.nim
@@ -1,7 +1,6 @@
 import options
 import terminal
 import uri
-import tables
 import strutils
 import unicode
 
@@ -47,11 +46,6 @@ type
     xend*: int
     fromx*: int
     fromy*: int
-    nodes*: seq[Node]
-    links*: seq[Node]
-    elements*: seq[Element]
-    idelements*: Table[string, Element]
-    selectedlink*: Node
     attrs*: TermAttributes
     document*: Document
     displaycontrols*: bool
@@ -182,6 +176,12 @@ func width(line: seq[FlexibleCell]): int =
   for c in line:
     result += c.rune.width()
 
+func acursorx(buffer: Buffer): int =
+  return max(0, buffer.cursorx - buffer.fromx)
+
+func acursory(buffer: Buffer): int =
+  return buffer.cursory - buffer.fromy
+
 func cellOrigin(buffer: Buffer, x: int, y: int): int =
   let row = y * buffer.width
   var ox = x
@@ -190,7 +190,11 @@ func cellOrigin(buffer: Buffer, x: int, y: int): int =
   return ox
 
 func currentCellOrigin(buffer: Buffer): int =
-  return buffer.cellOrigin(buffer.cursorx - buffer.fromx, buffer.cursory - buffer.fromy)
+  return buffer.cellOrigin(buffer.acursorx, buffer.acursory)
+
+func currentRune(buffer: Buffer): Rune =
+  let row = (buffer.cursory - buffer.fromy) * buffer.width
+  return buffer.display[row + buffer.currentCellOrigin()].runes[0]
 
 func cellWidthOverlap*(buffer: Buffer, x: int, y: int): int =
   let ox = buffer.cellOrigin(x, y)
@@ -213,9 +217,6 @@ func atPercentOf*(buffer: Buffer): int =
   if buffer.lines.len == 0: return 100
   return (100 * (buffer.cursory + 1)) div buffer.numLines
 
-func lastNode*(buffer: Buffer): Node =
-  return buffer.nodes[^1]
-
 func cursorOnNode*(buffer: Buffer, node: Node): bool =
   if node.y == node.ey and node.y == buffer.cursory:
     return buffer.cursorx >= node.x and buffer.cursorx < node.ex
@@ -230,14 +231,6 @@ func findSelectedElement*(buffer: Buffer): Option[HtmlElement] =
 func canScroll*(buffer: Buffer): bool =
   return buffer.numLines >= buffer.height
 
-func getElementById*(buffer: Buffer, id: string): Element =
-  if buffer.idelements.hasKey(id):
-    return buffer.idelements[id]
-  return nil
-
-proc findSelectedNode*(buffer: Buffer): Option[Node] =
-  discard #TODO
-
 proc addLine(buffer: Buffer) =
   buffer.lines.add(newSeq[FlexibleCell]())
 
@@ -245,21 +238,13 @@ proc clearText*(buffer: Buffer) =
   buffer.lines.setLen(0)
   buffer.addLine()
 
-proc clearNodes*(buffer: Buffer) =
-  buffer.nodes.setLen(0)
-  buffer.links.setLen(0)
-  buffer.elements.setLen(0)
-  buffer.idelements.clear()
-
 proc clearBuffer*(buffer: Buffer) =
   buffer.clearText()
-  buffer.clearNodes()
   buffer.cursorx = 0
   buffer.cursory = 0
   buffer.fromx = 0
   buffer.fromy = 0
   buffer.hovertext = ""
-  buffer.selectedlink = nil
 
 proc restoreCursorX(buffer: Buffer) =
   buffer.cursorx = max(min(buffer.currentLineWidth() - 1, buffer.xend), 0)
@@ -308,18 +293,24 @@ proc cursorUp*(buffer: Buffer) =
 
 proc cursorRight*(buffer: Buffer) =
   let cellwidth = buffer.currentCellWidth()
-  let cellorigin = buffer.currentCellOrigin()
+  let cellorigin = buffer.fromx + buffer.currentCellOrigin()
   let lw = buffer.currentLineWidth()
   if buffer.cursorx < lw - 1:
     buffer.cursorx = min(lw - 1, cellorigin + cellwidth)
+    assert buffer.cursorx >= 0
     buffer.xend = buffer.cursorx
     if buffer.cursorx - buffer.width >= buffer.fromx:
       inc buffer.fromx
       buffer.redraw = true
+    if buffer.cursorx == buffer.fromx:
+      inc buffer.cursorx
 
 proc cursorLeft*(buffer: Buffer) =
-  let cellorigin = buffer.currentCellOrigin()
+  eprint "??", buffer.currentCellOrigin(), buffer.fromx
+  let cellorigin = buffer.fromx + buffer.currentCellOrigin()
+  let lw = buffer.currentLineWidth()
   if buffer.fromx > buffer.cursorx:
+    buffer.cursorx = min(max(lw - 1, 0), cellorigin - 1)
     buffer.fromx = buffer.cursorx
     buffer.redraw = true
   elif buffer.cursorx > 0:
@@ -338,58 +329,46 @@ proc cursorLineBegin*(buffer: Buffer) =
     buffer.redraw = true
 
 proc cursorLineEnd*(buffer: Buffer) =
-  buffer.cursorx = buffer.currentLineWidth() - 1
+  buffer.cursorx = max(buffer.currentLineWidth() - 1, 0)
   buffer.xend = buffer.cursorx
   buffer.fromx = max(buffer.cursorx - buffer.width + 1, 0)
   buffer.redraw = buffer.fromx > 0
 
-iterator revnodes*(buffer: Buffer): Node {.inline.} =
-  var i = buffer.nodes.len - 1
-  while i >= 0:
-    yield buffer.nodes[i]
-    dec i
-
 proc cursorNextWord*(buffer: Buffer) =
   let llen = buffer.currentLineWidth() - 1
-  var x = buffer.cursorx
-  var y = buffer.cursory
   if llen >= 0:
 
-    while not buffer.lines[y][x].rune.breaksWord():
-      if x >= llen:
+    while not buffer.currentRune().breaksWord():
+      if buffer.cursorx >= llen:
         break
-      inc x
+      buffer.cursorRight()
 
-    while buffer.lines[y][x].rune.breaksWord():
-      if x >= llen:
+    while buffer.currentRune().breaksWord():
+      if buffer.cursorx >= llen:
         break
-      inc x
+      buffer.cursorRight()
 
-  if x >= llen:
-    if y < buffer.numLines:
-      inc y
-      x = 0
-  buffer.cursorTo(x, y)
+  if buffer.cursorx >= buffer.currentLineWidth() - 1:
+    if buffer.cursory < buffer.numLines - 1:
+      buffer.cursorDown()
+      buffer.cursorLineBegin()
 
 proc cursorPrevWord*(buffer: Buffer) =
-  var x = buffer.cursorx
-  var y = buffer.cursory
   if buffer.currentLineWidth() > 0:
-    while not buffer.lines[y][x].rune.breaksWord():
-      if x == 0:
+    while not buffer.currentRune().breaksWord():
+      if buffer.cursorx == 0:
         break
-      dec x
+      buffer.cursorLeft()
 
-    while buffer.lines[y][x].rune.breaksWord():
-      if x == 0:
+    while buffer.currentRune().breaksWord():
+      if buffer.cursorx == 0:
         break
-      dec x
+      buffer.cursorLeft()
 
-  if x == 0:
-    if y > 0:
-      dec y
-      x = buffer.lines[y].len - 1
-  buffer.cursorTo(x, y)
+  if buffer.cursorx == 0:
+    if buffer.cursory > 0:
+      buffer.cursorUp()
+      buffer.cursorLineEnd()
 
 proc cursorNextLink*(buffer: Buffer) =
   #TODO
@@ -425,7 +404,7 @@ proc cursorMiddle*(buffer: Buffer) =
   buffer.restoreCursorX()
 
 proc cursorBottom*(buffer: Buffer) =
-  buffer.cursory = min(buffer.fromy + buffer.height - 1, buffer.numLines)
+  buffer.cursory = min(buffer.fromy + buffer.height - 1, buffer.numLines - 1)
   buffer.restoreCursorX()
 
 proc centerLine*(buffer: Buffer) =
@@ -505,10 +484,12 @@ proc scrollLeft*(buffer: Buffer) =
     buffer.redraw = true
 
 proc gotoAnchor*(buffer: Buffer): bool =
-  if buffer.location.anchor != "":
-    let node =  buffer.getElementById(buffer.location.anchor)
-    if node != nil:
-      buffer.scrollTo(max(node.y - buffer.height div 2, 0))
+  discard
+  #TODO
+  #if buffer.location.anchor != "":
+  #  let node =  buffer.getElementById(buffer.location.anchor)
+  #  if node != nil:
+  #    buffer.scrollTo(max(node.y - buffer.height div 2, 0))
 
 proc setLocation*(buffer: Buffer, uri: Uri) =
   buffer.location = uri
@@ -587,9 +568,11 @@ proc updateCursor(buffer: Buffer) =
     buffer.fromy = 0
     buffer.cursory = buffer.lastVisibleLine - 1
 
+  if buffer.cursorx >= buffer.currentLineWidth() - 1:
+    buffer.cursorLineEnd()
+
   if buffer.lines.len == 0:
     buffer.cursory = 0
-    return
 
 proc clearDisplay*(buffer: Buffer) =
   var i = 0
@@ -602,7 +585,8 @@ proc refreshDisplay*(buffer: Buffer) =
   buffer.prevdisplay = buffer.display
   buffer.clearDisplay()
 
-  for line in buffer.lines[buffer.fromy..buffer.lastVisibleLine - 1]:
+  for line in buffer.lines[buffer.fromy..
+                           buffer.lastVisibleLine - 1]:
     var w = 0
     var i = 0
     while w < buffer.fromx and i < line.len:
@@ -641,6 +625,7 @@ proc renderPlainText*(buffer: Buffer, text: string) =
     inc i
   if line.len > 0:
     buffer.setText(0, y, line.toRunes())
+  buffer.updateCursor()
 
 proc renderDocument*(buffer: Buffer) =
   buffer.clearText()
@@ -653,7 +638,6 @@ proc renderDocument*(buffer: Buffer) =
     let box = stack.pop()
     if box of CSSInlineBox:
       let inline = CSSInlineBox(box)
-      var i = 0
       eprint "NEW BOX", inline.context.conty
       for line in inline.content:
         eprint line
@@ -665,6 +649,7 @@ proc renderDocument*(buffer: Buffer) =
     while i >= 0:
       stack.add(box.children[i])
       dec i
+  buffer.updateCursor()
 
 proc cursorBufferPos(buffer: Buffer) =
   let x = max(buffer.cursorx - buffer.fromx, 0)
diff --git a/src/layout/box.nim b/src/layout/box.nim
index bacaa1b1..f36823ae 100644
--- a/src/layout/box.nim
+++ b/src/layout/box.nim
@@ -18,19 +18,25 @@ type
     y*: int
     width*: int
     height*: int
-    bh*: int
-    bw*: int
     children*: seq[CSSBox]
-    context*: FormatContext
+    context*: InlineContext
+    bcontext*: BlockContext
 
-  FormatContext* = ref object
+  InlineContext* = ref object
     context*: FormatContextType
     fromx*: int
     fromy*: int
+    marginx*: int
+    marginy*: int
     conty*: bool
     whitespace*: bool
     cssvalues*: CSSComputedValues
 
+  BlockContext* = ref object
+    context*: FormatContextType
+    marginx*: int
+    marginy*: int
+
   CSSRowBox* = object
     x*: int
     y*: int
@@ -44,6 +50,7 @@ type
 
   CSSBlockBox* = ref CSSBlockBoxObj
   CSSBlockBoxObj = object of CSSBox
+    tag*: string
 
 func `+`(a: CSSRect, b: CSSRect): CSSRect =
   result.x1 = a.x1 + b.x1
diff --git a/src/layout/layout.nim b/src/layout/layout.nim
index fe96b416..4f73f53c 100644
--- a/src/layout/layout.nim
+++ b/src/layout/layout.nim
@@ -9,33 +9,53 @@ import io/buffer
 import io/cell
 import utils/twtstr
 
-func newContext*(box: CSSBox): FormatContext =
+func newContext*(box: CSSBox): InlineContext =
   new(result)
   result.fromx = box.x
   result.whitespace = true
 
-func newBlockBox*(parent: CSSBox): CSSBlockBox =
+func newBlockBox*(parent: CSSBox, vals: CSSComputedValues): CSSBlockBox =
   new(result)
+  result.bcontext = parent.bcontext #TODO make this something like state or something
   result.x = parent.x
   if parent.context.conty:
     inc parent.height
+    eprint "inc n"
+    inc parent.context.fromy
     parent.context.conty = false
-  result.y = parent.y + parent.height
+  result.y = parent.context.fromy
+  let mtop = vals[RULE_MARGIN_TOP].length.cells()
+  if mtop > parent.bcontext.marginy:
+    result.y += mtop - parent.bcontext.marginy
+    eprint "my", mtop, parent.bcontext.marginy
+    parent.bcontext.marginy = mtop
 
   result.width = parent.width
   result.context = newContext(parent)
+  eprint "inc to", result.y
+  result.context.fromy = result.y
+  result.context.cssvalues = vals
 
 func newInlineBox*(parent: CSSBox): CSSInlineBox =
   assert parent != nil
   new(result)
   result.x = parent.x
-  result.y = parent.y + parent.height
+  result.y = parent.context.fromy
 
   result.width = parent.width
   result.context = parent.context
+  result.bcontext = parent.bcontext
   if result.context == nil:
     result.context = newContext(parent)
 
+proc inlineWrap(ibox: var CSSInlineBox, rowi: var int, fromx: var int, rowbox: var CSSRowBox) =
+  ibox.content.add(rowbox)
+  inc rowi
+  fromx = ibox.x
+  ibox.context.whitespace = true
+  ibox.context.conty = true
+  rowbox = CSSRowBox(x: ibox.x, y: ibox.y + rowi)
+
 proc processInlineBox(parent: CSSBox, str: string): CSSBox =
   var ibox: CSSInlineBox
   var use_parent = false
@@ -51,22 +71,32 @@ proc processInlineBox(parent: CSSBox, str: string): CSSBox =
   var i = 0
   var rowi = 0
   var fromx = ibox.context.fromx
-  var rowbox = CSSRowBox(x: fromx, y: ibox.y + rowi)
+  var rowbox = CSSRowBox(x: fromx, y: ibox.context.fromy)
   var r: Rune
   while i < str.len:
     fastRuneAt(str, i, r)
     if rowbox.width + r.width() > ibox.width:
-      ibox.content.add(rowbox)
-      inc rowi
-      fromx = ibox.x
-      ibox.context.whitespace = true
-      ibox.context.conty = false
-      rowbox = CSSRowBox(x: ibox.x, y: ibox.y + rowi)
+      inlineWrap(ibox, rowi, fromx, rowbox)
     if r.isWhitespace():
       if ibox.context.whitespace:
         continue
       else:
-        ibox.context.whitespace = true
+        let wsr = ibox.context.cssvalues[RULE_WHITESPACE].whitespace
+
+        case wsr
+        of WHITESPACE_NORMAL, WHITESPACE_NOWRAP:
+          r = Rune(' ')
+        of WHITESPACE_PRE, WHITESPACE_PRE_LINE, WHITESPACE_PRE_WRAP:
+          if r == Rune('\n'):
+            inlineWrap(ibox, rowi, fromx, rowbox)
+            ibox.context.whitespace = false
+            continue
+
+        case wsr
+        of WHITESPACE_NORMAL, WHITESPACE_NOWRAP, WHITESPACE_PRE_LINE:
+          ibox.context.whitespace = true
+        else:
+          ibox.context.whitespace = false
     else:
       ibox.context.whitespace = false
     rowbox.width += r.width()
@@ -77,6 +107,10 @@ proc processInlineBox(parent: CSSBox, str: string): CSSBox =
     ibox.context.conty = true
 
   ibox.height += rowi
+  eprint "inc i", rowi, rowbox.runes
+  if rowi > 0 or rowbox.width > 0:
+    parent.bcontext.marginy = 0
+  ibox.context.fromy += rowi
   if use_parent:
     return nil
   return ibox
@@ -84,8 +118,9 @@ proc processInlineBox(parent: CSSBox, str: string): CSSBox =
 proc processElemBox(parent: CSSBox, elem: Element): CSSBox =
   case elem.cssvalues[RULE_DISPLAY].display
   of DISPLAY_BLOCK:
-    result = newBlockBox(parent)
-    result.context.cssvalues = elem.cssvalues
+    eprint "START", elem.tagType
+    result = newBlockBox(parent, elem.cssvalues)
+    CSSBlockBox(result).tag = $elem.tagType
   of DISPLAY_INLINE:
     result = newInlineBox(parent)
     result.context.cssvalues = elem.cssvalues
@@ -102,14 +137,23 @@ proc add(parent: var CSSBox, box: CSSBox) =
     parent.context.whitespace = true
     if box.context.conty:
       inc box.height
+      eprint "inc a"
+      inc box.context.fromy
+      box.context.conty = false
+    let mbot = box.context.cssvalues[RULE_MARGIN_BOTTOM].length.cells()
+    eprint "inc b", mbot
+    box.context.fromy += mbot
+    box.bcontext.marginy = mbot
+    eprint "END", CSSBlockBox(box).tag
   parent.height += box.height
+  eprint "parent to", box.context.fromy
+  parent.context.fromy = box.context.fromy
   parent.children.add(box)
 
 proc processPseudoBox(parent: CSSBox, cssvalues: CSSComputedValues): CSSBox =
   case cssvalues[RULE_DISPLAY].display
   of DISPLAY_BLOCK:
-    result = newBlockBox(parent)
-    result.context.cssvalues = cssvalues
+    result = newBlockBox(parent, cssvalues)
     result.add(processInlineBox(parent, $cssvalues[RULE_CONTENT].content)) 
   of DISPLAY_INLINE:
     result = processInlineBox(parent, $cssvalues[RULE_CONTENT].content)
@@ -147,5 +191,6 @@ proc processNode(parent: CSSBox, node: Node): CSSBox =
 proc alignBoxes*(buffer: Buffer) =
   buffer.rootbox = CSSBlockBox(x: 0, y: 0, width: buffer.width, height: 0)
   buffer.rootbox.context = newContext(buffer.rootbox)
+  buffer.rootbox.bcontext = new(BlockContext)
   for child in buffer.document.root.childNodes:
     buffer.rootbox.add(processNode(buffer.rootbox, child))
diff --git a/src/main.nim b/src/main.nim
index 9661ba10..4515147f 100644
--- a/src/main.nim
+++ b/src/main.nim
@@ -45,7 +45,7 @@ proc main*() =
   buffer.source = getPageUri(uri).readAll() #TODO get rid of this
   buffer.document = parseHtml(newStringStream(buffer.source))
   buffer.setLocation(uri)
-  buffer.document.applyDefaultStylesheet()
+  buffer.document.applyStylesheets()
   buffer.alignBoxes()
   buffer.renderDocument()
   var lastUri = uri
diff --git a/src/types/enums.nim b/src/types/enums.nim
index 3d79091b..914a4dc2 100644
--- a/src/types/enums.nim
+++ b/src/types/enums.nim
@@ -73,13 +73,13 @@ type
   CSSRuleType* = enum
     RULE_ALL, RULE_COLOR, RULE_MARGIN, RULE_MARGIN_TOP, RULE_MARGIN_LEFT,
     RULE_MARGIN_RIGHT, RULE_MARGIN_BOTTOM, RULE_FONT_STYLE, RULE_DISPLAY,
-    RULE_CONTENT
+    RULE_CONTENT, RULE_WHITESPACE
 
   CSSGlobalValueType* = enum
     VALUE_INITIAL, VALUE_INHERIT, VALUE_REVERT, VALUE_UNSET
 
   CSSValueType* = enum
-    VALUE_NONE, VALUE_LENGTH, VALUE_COLOR, VALUE_CONTENT, VALUE_DISPLAY, VALUE_FONT_STYLE
+    VALUE_NONE, VALUE_LENGTH, VALUE_COLOR, VALUE_CONTENT, VALUE_DISPLAY, VALUE_FONT_STYLE, VALUE_WHITESPACE
 
   DrawInstructionType* = enum
     DRAW_TEXT, DRAW_GOTO, DRAW_FGCOLOR, DRAW_BGCOLOR, DRAW_STYLE, DRAW_RESET
diff --git a/src/utils/eprint.nim b/src/utils/eprint.nim
index d13fcf91..eba5f51f 100644
--- a/src/utils/eprint.nim
+++ b/src/utils/eprint.nim
@@ -1,25 +1,27 @@
 {.used.}
 
 template eprint*(s: varargs[string, `$`]) = {.cast(noSideEffect).}:
-  var a = false
-  for x in s:
-    if not a:
-      a = true
-    else:
-      stderr.write(' ')
-    stderr.write(x)
-  stderr.write('\n')
+  if not defined(release):
+    var a = false
+    for x in s:
+      if not a:
+        a = true
+      else:
+        stderr.write(' ')
+      stderr.write(x)
+    stderr.write('\n')
 
 template eecho*(s: varargs[string, `$`]) = {.cast(noSideEffect).}:
-  var a = false
-  var o = ""
-  for x in s:
-    if not a:
-      a = true
-    else:
-      o &= ' '
-    o &= x
-  echo o
+  if not defined(release):
+    var a = false
+    var o = ""
+    for x in s:
+      if not a:
+        a = true
+      else:
+        o &= ' '
+      o &= x
+    echo o
 
 template print*(s: varargs[string, `$`]) =
   for x in s: