about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2021-01-20 23:26:29 +0100
committerbptato <nincsnevem662@gmail.com>2021-01-20 23:26:29 +0100
commit6e1ef74bdca4c629bda2756a6e72279e038195f4 (patch)
treeea06c329dbf98e55803887c9780703f9201ff987
parent07ee263ccc895b543d0b017f1446e9121bc8a1bb (diff)
downloadchawan-6e1ef74bdca4c629bda2756a6e72279e038195f4.tar.gz
some things just never work
-rw-r--r--Makefile2
-rw-r--r--buffer.nim108
-rw-r--r--display.nim129
-rw-r--r--htmlelement.nim191
-rw-r--r--main.nim10
-rw-r--r--twtio.nim2
-rw-r--r--twtstr.nim15
7 files changed, 237 insertions, 220 deletions
diff --git a/Makefile b/Makefile
index 68cb1ff5..b0297753 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,5 @@
 all: build
 build:
 	nim compile -d:ssl -o:twt main.nim
+release:
+	nim compile -d:release -d:ssl -o:twt main.nim
diff --git a/buffer.nim b/buffer.nim
index 5f4cf227..cc0fafc9 100644
--- a/buffer.nim
+++ b/buffer.nim
@@ -28,6 +28,7 @@ type
     fromY*: int
     nodes*: seq[HtmlNode]
     links*: seq[HtmlNode]
+    clickables*: seq[HtmlNode]
     elements*: seq[HtmlNode]
     idelements*: Table[string, HtmlNode]
     selectedlink*: HtmlNode
@@ -84,7 +85,7 @@ func atPercentOf*(buffer: Buffer): int =
   return (100 * buffer.cursorY) div buffer.lastLine()
 
 func visibleText*(buffer: Buffer): string = 
-  return buffer.text.substr(buffer.lines[buffer.fromY], buffer.lines[buffer.lastVisibleLine() - 1])
+  return buffer.text.substr(buffer.lines[buffer.fromY], buffer.lines[buffer.lastVisibleLine() - 1] - 2)
 
 func lastNode*(buffer: Buffer): HtmlNode =
   return buffer.nodes[^1]
@@ -95,13 +96,17 @@ func onNewLine*(buffer: Buffer): bool =
 func onSpace*(buffer: Buffer): bool =
   return buffer.text.len > 0 and buffer.text[^1] == ' '
 
+func cursorOnNode*(buffer: Buffer, node: HtmlNode): bool =
+    return buffer.cursorY >= node.y and buffer.cursorY < node.y + node.height and
+           buffer.cursorX >= node.x and buffer.cursorX < node.x + node.width
+
 func findSelectedElement*(buffer: Buffer): Option[HtmlElement] =
   if buffer.selectedlink != nil:
     return some(buffer.selectedlink.text.parent)
   for node in buffer.nodes:
-    if node.nodeType == NODE_ELEMENT:
-      if node.element.formattedElem.len > 0:
-        if buffer.cursorY >= node.y and buffer.cursorY <= node.y + node.height and buffer.cursorX >= node.x and buffer.cursorX <= node.x + node.width: return some(node.element)
+    if node.isElemNode():
+      if node.getFmtLen() > 0:
+        if buffer.cursorOnNode(node): return some(node.element)
   return none(HtmlElement)
 
 func cursorAt*(buffer: Buffer): int =
@@ -113,9 +118,6 @@ func cursorChar*(buffer: Buffer): char =
 func canScroll*(buffer: Buffer): bool =
   return buffer.lastLine() > buffer.height
 
-func cursorOnNode*(buffer: Buffer, node: HtmlNode): bool =
-    return buffer.cursorY >= node.y and buffer.cursorY <= node.y + node.height and buffer.cursorX >= node.x and buffer.cursorX <= node.x + node.width
-
 func getElementById*(buffer: Buffer, id: string): HtmlNode =
   if buffer.idelements.hasKey(id):
     return buffer.idelements[id]
@@ -123,15 +125,28 @@ func getElementById*(buffer: Buffer, id: string): HtmlNode =
 
 proc findSelectedNode*(buffer: Buffer): Option[HtmlNode] =
   for node in buffer.nodes:
-    if node.getFormattedLen() > 0 and node.displayed():
+    if node.getFmtLen() > 0 and node.displayed():
       if buffer.cursorY >= node.y and buffer.cursorY <= node.y + node.height and buffer.cursorX >= node.x and buffer.cursorX <= node.x + node.width:
         return some(node)
   return none(HtmlNode)
 
 proc addNode*(buffer: Buffer, htmlNode: HtmlNode) =
   buffer.nodes.add(htmlNode)
-  if htmlNode.isTextNode() and htmlNode.text.parent.htmlTag == tagA:
+
+  if htmlNode.isTextNode() and htmlNode.text.parent.islink:
     buffer.links.add(htmlNode)
+
+  if htmlNode.isElemNode():
+    case htmlNode.element.htmlTag
+    of tagInput, tagOption:
+      if not htmlNode.element.hidden:
+        buffer.clickables.add(htmlNode)
+    else: discard
+  elif htmlNode.isTextNode():
+    if htmlNode.text.parent.islink:
+      let anchor = htmlNode.text.parent.getParent(tagA)
+      buffer.clickables.add(anchor.node)
+
   if htmlNode.isElemNode():
     buffer.elements.add(htmlNode)
     if htmlNode.element.id != "":
@@ -170,6 +185,7 @@ proc clearText*(buffer: Buffer) =
 proc clearNodes*(buffer: Buffer) =
   buffer.nodes.setLen(0)
   buffer.links.setLen(0)
+  buffer.clickables.setLen(0)
   buffer.elements.setLen(0)
   buffer.idelements.clear()
 
@@ -182,6 +198,23 @@ proc clearBuffer*(buffer: Buffer) =
   buffer.fromY = 0
   buffer.hovertext = ""
 
+proc scrollTo*(buffer: Buffer, y: int): bool =
+  if y == buffer.fromY:
+    return false
+  buffer.fromY = min(max(buffer.lastLine() - buffer.height + 1, 0), y)
+  buffer.cursorY = min(max(buffer.fromY, buffer.cursorY), buffer.fromY + buffer.height)
+  return true
+
+proc cursorTo*(buffer: Buffer, x: int, y: int): bool =
+  buffer.cursorY = min(max(y, 0), buffer.lastLine())
+  if buffer.fromY > buffer.cursorY:
+    buffer.fromY = min(buffer.cursorY, buffer.lastLine() - buffer.height)
+  elif buffer.fromY + buffer.height < buffer.cursorY:
+    buffer.fromY = max(buffer.cursorY - buffer.height, 0)
+  buffer.cursorX = min(max(x, 0), buffer.currentRawLineLength())
+  buffer.fromX = min(max(buffer.currentRawLineLength() - buffer.width + 1, 0), 0) #TODO
+  return true
+
 proc cursorDown*(buffer: Buffer): bool =
   if buffer.cursorY < buffer.lastLine():
     buffer.cursorY += 1
@@ -302,44 +335,23 @@ proc cursorPrevWord*(buffer: Buffer): bool =
       return false
     discard buffer.cursorLeft()
 
-iterator revNodes*(buffer: Buffer): HtmlNode {.inline.} =
-  var i = buffer.nodes.len - 1
+iterator revclickables*(buffer: Buffer): HtmlNode {.inline.} =
+  var i = buffer.clickables.len - 1
   while i >= 0:
-    yield buffer.nodes[i]
+    yield buffer.clickables[i]
     i -= 1
 
 proc cursorNextLink*(buffer: Buffer): bool =
-  var next = false
-  for node in buffer.nodes:
-    if node.nodeType == NODE_ELEMENT:
-      case node.element.htmlTag
-      of tagInput, tagA:
-        if next:
-          var res = false
-          while buffer.cursorY < node.y:
-            res = res or buffer.cursorDown()
-          buffer.cursorLineBegin()
-          while buffer.cursorX < node.x:
-            res = res or buffer.cursorRight()
-          return res
-        if buffer.cursorY >= node.y and buffer.cursorY <= node.y + node.height and buffer.cursorX >= node.x and buffer.cursorX <= node.x + node.width:
-          next = true
-      else: discard
+  for node in buffer.clickables:
+    if node.y > buffer.cursorY or (node.y == buffer.cursorY and node.x > buffer.cursorX):
+      return buffer.cursorTo(node.x, node.y)
+  return false
 
 proc cursorPrevLink*(buffer: Buffer): bool =
-  var next = false
-  for node in buffer.revNodes:
-    if node.nodeType == NODE_ELEMENT and node.element.htmlTag == tagInput:
-      if next:
-        var res = false
-        while buffer.cursorY < node.y:
-          res = res or buffer.cursorDown()
-        buffer.cursorLineBegin()
-        while buffer.cursorX < node.x:
-          res = res or buffer.cursorRight()
-        return true
-      if buffer.cursorY >= node.y and buffer.cursorY <= node.y + node.height and buffer.cursorX >= node.x and buffer.cursorX <= node.x + node.width:
-        next = true
+  for node in buffer.revclickables:
+    if node.y < buffer.cursorY or (node.y == buffer.cursorY and node.x < buffer.cursorX):
+      return buffer.cursorTo(node.x, node.y)
+  return false
 
 proc cursorFirstLine*(buffer: Buffer): bool =
   if buffer.fromY > 0:
@@ -394,12 +406,6 @@ proc cursorBottom*(buffer: Buffer): bool =
   buffer.cursorY = min(buffer.fromY + buffer.height - 1, buffer.lastLine())
   return false
 
-proc scrollTo*(buffer: Buffer, y: int): bool =
-  if y == buffer.fromY:
-    return false
-  buffer.fromY = min(max(buffer.lastLine() - buffer.height + 1, 0), y - buffer.height)
-  return true
-
 proc scrollDown*(buffer: Buffer): bool =
   if buffer.fromY + buffer.height <= buffer.lastLine():
     buffer.fromY += 1
@@ -423,13 +429,17 @@ proc checkLinkSelection*(buffer: Buffer): bool =
     if buffer.cursorOnNode(buffer.selectedlink):
       return false
     else:
+      let anchor = buffer.selectedlink.text.parent.getParent(tagA)
+      anchor.selected = false
       buffer.selectedlink = nil
       buffer.hovertext = ""
   for node in buffer.links:
     if buffer.cursorOnNode(node):
       buffer.selectedlink = node
-      node.text.parent.selected = true
-      buffer.hovertext = node.text.parent.href
+      let anchor = node.text.parent.getParent(tagA)
+      assert(anchor != nil)
+      anchor.selected = true
+      buffer.hovertext = anchor.href
       return true
   return false
 
diff --git a/display.nim b/display.nim
index f4eaa806..898993dd 100644
--- a/display.nim
+++ b/display.nim
@@ -22,10 +22,10 @@ proc statusMsg*(str: string, at: int) =
   print(str.addAnsiStyle(styleReverse))
 
 type
-  RenderState = ref RenderStateObj
-  RenderStateObj = object
+  RenderState = object
     x: int
     y: int
+    lastwidth: int
     atchar: int
     atrawchar: int
     centerqueue: int
@@ -34,17 +34,13 @@ type
     blankspaces: int
     nextspaces: int
     docenter: bool
+    indent: int
+    listval: int
 
 func newRenderState(): RenderState =
-  return RenderState()
+  return RenderState(y: 1)
 
-func nodeAttr(node: HtmlNode): HtmlElement =
-  case node.nodeType
-  of NODE_TEXT: return node.text.parent
-  of NODE_ELEMENT: return node.element
-  else: assert(false)
-
-proc flushLine(buffer: Buffer, state: RenderState) =
+proc flushLine(buffer: Buffer, state: var RenderState) =
   if buffer.onNewLine():
     state.blanklines += 1
   buffer.write('\n')
@@ -57,7 +53,7 @@ proc flushLine(buffer: Buffer, state: RenderState) =
   buffer.rawlines.add(state.atrawchar)
   assert(buffer.onNewLine())
 
-proc addSpaces(buffer: Buffer, state: RenderState, n: int) =
+proc addSpaces(buffer: Buffer, state: var RenderState, n: int) =
   if state.x + n > buffer.width:
     buffer.flushLine(state)
     return
@@ -67,29 +63,27 @@ proc addSpaces(buffer: Buffer, state: RenderState, n: int) =
   state.atchar += n
   state.atrawchar += n
 
-proc addSpace(buffer: Buffer, state: RenderState) =
+proc addSpace(buffer: Buffer, state: var RenderState) =
   buffer.addSpaces(state, 1)
 
-proc assignCoords(node: HtmlNode, state: RenderState) =
-  node.x = state.x
-  node.y = state.y
-
-proc addSpacePadding(buffer: Buffer, state: RenderState) =
+proc addSpacePadding(buffer: Buffer, state: var RenderState) =
   if not buffer.onSpace():
     buffer.addSpace(state)
 
-proc writeWrappedText(buffer: Buffer, state: RenderState, fmttext: string, rawtext: string) =
+proc writeWrappedText(buffer: Buffer, state: var RenderState, fmttext: string, rawtext: string) =
+  state.lastwidth = 0
   var n = 0
   var fmtword = ""
   var rawword = ""
   var prevl = false
   for c in fmttext:
     fmtword &= c
+
     if n >= rawtext.len or rawtext[n] != c:
       continue
 
-    state.x += 1
     rawword &= c
+    state.x += 1
 
     if state.x > buffer.width:
       if buffer.rawtext.len > 0 and buffer.rawtext[^1] == ' ':
@@ -98,8 +92,11 @@ proc writeWrappedText(buffer: Buffer, state: RenderState, fmttext: string, rawte
         state.atchar -= 1
         state.atrawchar -= 1
         state.x -= 1
+      state.lastwidth = max(state.lastwidth, state.x)
       buffer.flushLine(state)
       prevl = true
+    else:
+      state.lastwidth = max(state.lastwidth, state.x)
 
     if c == ' ':
       buffer.writefmt(fmtword)
@@ -107,17 +104,19 @@ proc writeWrappedText(buffer: Buffer, state: RenderState, fmttext: string, rawte
       state.atchar += fmtword.len
       state.atrawchar += rawword.len
       if prevl:
-        state.x += fmtword.len
+        state.x += rawword.len
         prevl = false
       fmtword = ""
       rawword = ""
     n += 1
+
   buffer.writefmt(fmtword)
   buffer.writeraw(rawword)
   state.atchar += fmtword.len
   state.atrawchar += rawword.len
+  state.lastwidth = max(state.lastwidth, state.x)
 
-proc preAlignNode(buffer: Buffer, node: HtmlNode, state: RenderState) =
+proc preAlignNode(buffer: Buffer, node: HtmlNode, state: var RenderState) =
   let elem = node.nodeAttr()
   if not buffer.onNewLine() and node.openblock and state.blanklines == 0:
     buffer.flushLine(state)
@@ -125,21 +124,39 @@ proc preAlignNode(buffer: Buffer, node: HtmlNode, state: RenderState) =
   if node.openblock:
     while state.blanklines < max(elem.margin, elem.margintop):
       buffer.flushLine(state)
+    if elem.display == DISPLAY_LIST_ITEM:
+      eprint "???"
+      state.indent += 1
 
   if not buffer.onNewLine() and state.blanklines == 0 and node.displayed():
     buffer.addSpaces(state, state.nextspaces)
     state.nextspaces = 0
-    if elem.pad:
-      buffer.addSpacePadding(state)
-    
     if state.blankspaces < max(elem.margin, elem.marginleft):
       buffer.addSpaces(state, max(elem.margin, elem.marginleft) - state.blankspaces)
 
   if elem.centered and buffer.onNewLine() and node.displayed():
     buffer.addSpaces(state, max(buffer.width div 2 - state.centerlen div 2, 0))
     state.centerlen = 0
+  
+  if elem.display == DISPLAY_LIST_ITEM and state.indent > 0:
+    var listchar = ""
+    eprint "Display", listchar, elem.parent.htmlTag
+    case elem.parent.htmlTag
+    of tagUl:
+      listchar = "*"
+    of tagOl:
+      state.listval += 1
+      listchar = $state.listval & ")"
+    else:
+      return
+    buffer.addSpaces(state, state.indent)
+    buffer.write(listchar)
+    state.x += 1
+    state.atchar += 1
+    state.atrawchar += 1
+    buffer.addSpaces(state, 1)
 
-proc postAlignNode(buffer: Buffer, node: HtmlNode, state: RenderState) =
+proc postAlignNode(buffer: Buffer, node: HtmlNode, state: var RenderState) =
   let elem = node.nodeAttr()
 
   if node.getRawLen() > 0:
@@ -147,8 +164,6 @@ proc postAlignNode(buffer: Buffer, node: HtmlNode, state: RenderState) =
     state.blankspaces = 0
 
   if not buffer.onNewLine() and state.blanklines == 0:
-    if elem.pad:
-      state.nextspaces = 1
     state.nextspaces += max(elem.margin, elem.marginright)
     if node.closeblock:
       buffer.flushLine(state)
@@ -156,26 +171,26 @@ proc postAlignNode(buffer: Buffer, node: HtmlNode, state: RenderState) =
   if node.closeblock:
     while state.blanklines < max(elem.margin, elem.marginbottom):
       buffer.flushLine(state)
+    if elem.display == DISPLAY_LIST_ITEM and node.isTextNode():
+      state.indent -= 1
 
   if elem.htmlTag == tagBr and not node.openblock:
     buffer.flushLine(state)
 
-proc renderNode(buffer: Buffer, node: HtmlNode, state: RenderState) =
+  if elem.display == DISPLAY_LIST_ITEM and node.isElemNode():
+    buffer.flushLine(state)
+
+proc renderNode(buffer: Buffer, node: HtmlNode, state: var RenderState) =
   if not node.visibleNode():
     return
-  if node.isElemNode():
-    node.element.formattedElem = node.element.getFormattedElem()
   let elem = node.nodeAttr()
   if elem.htmlTag == tagTitle:
-    if isTextNode(node):
-      buffer.title = node.text.text
+    if node.isTextNode():
+      buffer.title = node.rawtext
     return
   else: discard
   if elem.hidden: return
 
-  node.height = 1
-  node.width = node.getRawLen()
-
   if not state.docenter:
     if elem.centered:
       if not node.closeblock and elem.htmlTag != tagBr:
@@ -195,18 +210,19 @@ proc renderNode(buffer: Buffer, node: HtmlNode, state: RenderState) =
 
   buffer.preAlignNode(node, state)
 
-  node.assignCoords(state)
-  if isTextNode(node):
-    buffer.writeWrappedText(state, node.text.formattedText, node.text.text)
-  elif isElemNode(node):
-    buffer.writeWrappedText(state, node.element.formattedElem, node.element.rawElem)
+  node.x = state.x
+  node.y = state.y
+  buffer.writeWrappedText(state, node.fmttext, node.rawtext)
+  node.width = state.lastwidth - node.x + 1
+  node.height = state.y - node.y + 1
 
   buffer.postAlignNode(node, state)
 
 iterator revItems*(n: XmlNode): XmlNode {.inline.} =
   var i = n.len - 1
   while i >= 0:
-    yield n[i]
+    if n[i].kind != xnComment:
+      yield n[i]
     i -= 1
 
 type
@@ -215,13 +231,13 @@ type
     xml*: XmlNode
     html*: HtmlNode
 
-proc setLastHtmlLine(buffer: Buffer, state: RenderState) =
+proc setLastHtmlLine(buffer: Buffer, state: var RenderState) =
   if buffer.text.len != buffer.lines[^1]:
     state.atchar = buffer.text.len + 1
     state.atrawchar = buffer.rawtext.len + 1
   buffer.flushLine(state)
 
-proc renderHtml(buffer: Buffer) =
+proc renderHtml*(buffer: Buffer) =
   var stack: seq[XmlHtmlNode]
   let first = XmlHtmlNode(xml: buffer.htmlSource,
                          html: getHtmlNode(buffer.htmlSource, none(HtmlElement)))
@@ -230,21 +246,22 @@ proc renderHtml(buffer: Buffer) =
   var state = newRenderState()
   while stack.len > 0:
     let currElem = stack.pop()
-    if currElem.html.nodeType != NODE_COMMENT:
-      buffer.renderNode(currElem.html, state)
-      if currElem.html.isElemNode():
-        if currElem.html.element.id != "":
-          eprint currElem.html.element.id
-      buffer.addNode(currElem.html)
+    buffer.renderNode(currElem.html, state)
+    buffer.addNode(currElem.html)
     var last = false
+    var prev: HtmlNode = nil
     for item in currElem.xml.revItems:
       let child = XmlHtmlNode(xml: item,
                               html: getHtmlNode(item, some(currElem.html.element)))
       stack.add(child)
+      child.html.prev = prev
+      prev = child.html
       if not last and child.html.visibleNode():
         last = true
         if currElem.html.element.display == DISPLAY_BLOCK:
           stack[^1].html.closeblock = true
+      else:
+        child.html.next = stack[^1].html
     if last:
       if currElem.html.element.display == DISPLAY_BLOCK:
         stack[^1].html.openblock = true
@@ -257,9 +274,11 @@ proc drawHtml(buffer: Buffer) =
   buffer.setLastHtmlLine(state)
 
 proc statusMsgForBuffer(buffer: Buffer) =
-  let msg = $buffer.cursorY & "/" & $buffer.lastLine() & " (" &
+  var msg = $buffer.cursorY & "/" & $buffer.lastLine() & " (" &
             $buffer.atPercentOf() & "%) " &
-            "<" & buffer.title & buffer.hovertext
+            "<" & buffer.title & ">"
+  if buffer.hovertext.len > 0:
+    msg &= " " & buffer.hovertext
   statusMsg(msg.maxString(buffer.width), buffer.height)
 
 proc cursorBufferPos(buffer: Buffer) =
@@ -326,10 +345,11 @@ proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool =
           if status:
             reshape = true
             redraw = true
-        of tagA:
-          buffer.setLocation(parseUri(buffer.selectedlink.text.parent.href))
-          return true
         else: discard
+        if selectedElem.get().islink:
+          let anchor = buffer.selectedlink.text.parent.getParent(tagA).href
+          buffer.setLocation(parseUri(anchor))
+          return true
     of ACTION_CHANGE_LOCATION:
       var url = $buffer.location
       clearStatusMsg(buffer.height)
@@ -358,7 +378,6 @@ proc displayPage*(attrs: TermAttributes, buffer: Buffer): bool =
   termGoto(0, 0)
   #buffer.printwrite = true
   discard buffer.gotoAnchor()
-  buffer.renderHtml()
   buffer.displayBuffer()
   buffer.statusMsgForBuffer()
   return inputLoop(attrs, buffer)
diff --git a/htmlelement.nim b/htmlelement.nim
index 4cf2e02e..35430f1e 100644
--- a/htmlelement.nim
+++ b/htmlelement.nim
@@ -15,7 +15,7 @@ type
     NODE_ELEMENT, NODE_TEXT, NODE_COMMENT
   DisplayType* =
     enum
-    DISPLAY_INLINE, DISPLAY_BLOCK, DISPLAY_SINGLE, DISPLAY_NONE
+    DISPLAY_INLINE, DISPLAY_BLOCK, DISPLAY_SINGLE, DISPLAY_LIST_ITEM, DISPLAY_NONE
   InputType* =
     enum
     INPUT_BUTTON, INPUT_CHECKBOX, INPUT_COLOR, INPUT_DATE, INPUT_DATETIME_LOCAL,
@@ -33,10 +33,9 @@ type
   HtmlText* = ref HtmlTextObj
   HtmlTextObj = object
     parent*: HtmlElement
-    text*: string
-    formattedText*: string
   HtmlElement* = ref HtmlElementObj
   HtmlElementObj = object
+    node*: HtmlNode
     id*: string
     name*: string
     value*: string
@@ -44,19 +43,17 @@ type
     hidden*: bool
     display*: DisplayType
     innerText*: string
-    formattedElem*: string
-    rawElem*: string
     textNodes*: int
     margintop*: int
     marginbottom*: int
     marginleft*: int
     marginright*: int
     margin*: int
-    pad*: bool
     bold*: bool
     italic*: bool
     underscore*: bool
-    parentElement*: HtmlElement
+    islink*: bool
+    parent*: HtmlElement
     case htmlTag*: HtmlTag
     of tagInput:
       itype*: InputType
@@ -66,8 +63,6 @@ type
       selected*: bool
     else:
       discard
-
-type
   HtmlNode* = ref HtmlNodeObj
   HtmlNodeObj = object
     case nodeType*: NodeType
@@ -77,12 +72,25 @@ type
       text*: HtmlText
     of NODE_COMMENT:
       comment*: string
+    rawtext*: string
+    fmttext*: string
     x*: int
     y*: int
     width*: int
     height*: int
     openblock*: bool
     closeblock*: bool
+    next*: HtmlNode
+    prev*: HtmlNode
+
+func nodeAttr*(node: HtmlNode): HtmlElement =
+  case node.nodeType
+  of NODE_TEXT: return node.text.parent
+  of NODE_ELEMENT: return node.element
+  else: assert(false)
+
+func displayed*(node: HtmlNode): bool =
+  return node.rawtext.len > 0 and node.nodeAttr().display != DISPLAY_NONE
 
 func isTextNode*(node: HtmlNode): bool =
   return node.nodeType == NODE_TEXT
@@ -90,33 +98,11 @@ func isTextNode*(node: HtmlNode): bool =
 func isElemNode*(node: HtmlNode): bool =
   return node.nodeType == NODE_ELEMENT
 
-func getFormattedLen*(htmlText: HtmlText): int =
-  return htmlText.formattedText.strip().len
-
-func getFormattedLen*(htmlElem: HtmlElement): int =
-  return htmlElem.formattedElem.len
-
-func getFormattedLen*(htmlNode: HtmlNode): int =
-  case htmlNode.nodeType
-  of NODE_TEXT: return htmlNode.text.getFormattedLen()
-  of NODE_ELEMENT: return htmlNode.element.getFormattedLen()
-  else:
-    assert(false)
-    return 0
-
-func getRawLen*(htmlText: HtmlText): int =
-  return htmlText.text.len
-
-func getRawLen*(htmlElem: HtmlElement): int =
-  return htmlElem.rawElem.len
+func getFmtLen*(htmlNode: HtmlNode): int =
+  return htmlNode.fmttext.len
 
 func getRawLen*(htmlNode: HtmlNode): int =
-  case htmlNode.nodeType
-  of NODE_TEXT: return htmlNode.text.getRawLen()
-  of NODE_ELEMENT: return htmlNode.element.getRawLen()
-  else:
-    assert(false)
-    return 0
+  return htmlNode.rawtext.len
 
 func visibleNode*(node: HtmlNode): bool =
   case node.nodeType
@@ -124,18 +110,6 @@ func visibleNode*(node: HtmlNode): bool =
   of NODE_ELEMENT: return true
   else: return false
 
-func displayed*(elem: HtmlElement): bool =
-  return elem.display != DISPLAY_NONE and (elem.getFormattedLen() > 0 or elem.htmlTag == tagBr) and not elem.hidden
-
-func displayed*(node: HtmlNode): bool =
-  if node.isTextNode():
-    return node.getRawLen() > 0
-  elif node.isElemNode():
-    return node.element.displayed()
-
-func empty*(elem: HtmlElement): bool =
-  return elem.textNodes == 0 or not elem.displayed()
-
 func toInputType*(str: string): InputType =
   case str
   of "button": INPUT_BUTTON
@@ -170,21 +144,23 @@ func toInputSize*(str: string): int =
 func getInputElement(xmlElement: XmlNode, htmlElement: HtmlElement): HtmlElement =
   assert(htmlElement.htmlTag == tagInput)
   htmlElement.itype = xmlElement.attr("type").toInputType()
+  if htmlElement.itype == INPUT_HIDDEN:
+    htmlElement.hidden = true
   htmlElement.size = xmlElement.attr("size").toInputSize()
   htmlElement.value = xmlElement.attr("value")
-  htmlElement.pad = true
   return htmlElement
 
 func getAnchorElement(xmlElement: XmlNode, htmlElement: HtmlElement): HtmlElement =
   assert(htmlElement.htmlTag == tagA)
   htmlElement.href = xmlElement.attr("href")
+  htmlElement.islink = true
   return htmlElement
 
 func getSelectElement(xmlElement: XmlNode, htmlElement: HtmlElement): HtmlElement =
   assert(htmlElement.htmlTag == tagSelect)
   for item in xmlElement.items:
     if item.kind == xnElement:
-      if item.tag == "option" and item.attr("value") != "":
+      if item.tag == "option":
         htmlElement.value = item.attr("value")
         break
   htmlElement.name = xmlElement.attr("name")
@@ -193,7 +169,7 @@ func getSelectElement(xmlElement: XmlNode, htmlElement: HtmlElement): HtmlElemen
 func getOptionElement(xmlElement: XmlNode, htmlElement: HtmlElement): HtmlElement =
   assert(htmlElement.htmlTag == tagOption)
   htmlElement.value = xmlElement.attr("value")
-  if htmlElement.parentElement.value != htmlElement.value:
+  if htmlElement.parent.value != htmlElement.value:
     htmlElement.hidden = true
   return htmlElement
 
@@ -201,53 +177,69 @@ func getFormattedInput(htmlElement: HtmlElement): string =
   case htmlElement.itype
   of INPUT_TEXT, INPUT_SEARCH:
     let valueFit = fitValueToSize(htmlElement.value, htmlElement.size)
-    return "[" & valueFit.addAnsiStyle(styleUnderscore).addAnsiFgColor(fgRed) & "]"
-  of INPUT_SUBMIT: return ("[" & htmlElement.value & "]").addAnsiFgColor(fgRed)
+    return valueFit.addAnsiStyle(styleUnderscore).buttonStr()
+  of INPUT_SUBMIT:
+    return htmlElement.value.buttonStr()
   else: discard
 
 func getRawInput(htmlElement: HtmlElement): string =
   case htmlElement.itype
   of INPUT_TEXT, INPUT_SEARCH:
     return "[" & htmlElement.value.fitValueToSize(htmlElement.size) & "]"
-  of INPUT_SUBMIT: return "[" & htmlElement.value & "]"
+  of INPUT_SUBMIT:
+    return "[" & htmlElement.value & "]"
   else: discard
 
-func getRawElem*(htmlElement: HtmlElement): string =
-  case htmlElement.htmlTag
-  of tagInput: return htmlElement.getRawInput()
-  of tagOption: return "[]"
-  else: return ""
-
-func getFormattedElem*(htmlElement: HtmlElement): string =
-  case htmlElement.htmlTag
-  of tagInput: return htmlElement.getFormattedInput()
-  else: return ""
-
-func getRawText*(htmlText: HtmlText): string =
-  if htmlText.parent.htmlTag != tagPre:
-    result = htmlText.text.replace(re"\n").strip()
+func getParent*(htmlElement: HtmlElement, htmlTag: HtmlTag): HtmlElement =
+  result = htmlElement
+  while result != nil and result.htmlTag != htmlTag:
+    result = result.parent
+
+func getRawText*(htmlNode: HtmlNode): string =
+  if htmlNode.isElemNode():
+    case htmlNode.element.htmlTag
+    of tagInput: return htmlNode.element.getRawInput()
+    else: return ""
+  elif htmlNode.isTextNode():
+    if htmlNode.text.parent.htmlTag != tagPre:
+      result = htmlNode.rawtext.replace(re"\n")
+      if result.strip().len > 0:
+        if htmlNode.nodeAttr().display != DISPLAY_INLINE:
+          if htmlNode.prev == nil or htmlNode.prev.nodeAttr().display != DISPLAY_INLINE:
+            result = result.strip(true, false)
+          if htmlNode.next == nil or htmlNode.next.nodeAttr().display != DISPLAY_INLINE:
+            result = result.strip(false, true)
+      else:
+        result = ""
+    else:
+      result = htmlNode.rawtext.strip()
+    if htmlNode.text.parent.htmlTag == tagOption:
+      result = "[" & result & "]"
   else:
-    result = htmlText.text
-
-  if htmlText.parent.htmlTag == tagOption:
-    result = "[" & result & "]"
+    assert(false)
 
-func getFormattedText*(htmlText: HtmlText): string =
-  result = htmlText.text
-  case htmlText.parent.htmlTag
-  of tagA:
-    result = result.addAnsiFgColor(fgBlue)
-    if htmlText.parent.selected:
+func getFmtText*(htmlNode: HtmlNode): string =
+  if htmlNode.isElemNode():
+    case htmlNode.element.htmlTag
+    of tagInput: return htmlNode.element.getFormattedInput()
+    else: return ""
+  elif htmlNode.isTextNode():
+    result = htmlNode.rawtext
+    if htmlNode.text.parent.islink:
+      result = result.addAnsiFgColor(fgBlue)
+      let parent = htmlNode.text.parent.getParent(tagA)
+      if parent != nil and parent.selected:
+        result = result.addAnsiStyle(styleUnderscore)
+
+    if htmlNode.text.parent.htmlTag == tagOption:
+      result = result.addAnsiFgColor(fgRed)
+
+    if htmlNode.text.parent.bold:
+      result = result.addAnsiStyle(styleBright)
+    if htmlNode.text.parent.italic:
+      result = result.addAnsiStyle(styleItalic)
+    if htmlNode.text.parent.underscore:
       result = result.addAnsiStyle(styleUnderscore)
-  of tagOption: result = result.addAnsiFgColor(fgRed)
-  else: discard
-
-  if htmlText.parent.bold:
-    result = result.addAnsiStyle(styleBright)
-  if htmlText.parent.italic:
-    result = result.addAnsiStyle(styleItalic)
-  if htmlText.parent.underscore:
-    result = result.addAnsiStyle(styleUnderscore)
 
 proc newElemFromParent(elem: HtmlElement, parentOpt: Option[HtmlElement]): HtmlElement =
   if parentOpt.isSome:
@@ -263,8 +255,8 @@ proc newElemFromParent(elem: HtmlElement, parentOpt: Option[HtmlElement]): HtmlE
     #elem.marginbottom = parent.marginbottom
     #elem.marginleft = parent.marginleft
     #elem.marginright = parent.marginright
-    elem.parentElement = parent
-  elem.pad = false
+    elem.parent = parent
+    elem.islink = parent.islink
 
   return elem
 
@@ -278,9 +270,10 @@ proc getHtmlElement*(xmlElement: XmlNode, inherit: Option[HtmlElement]): HtmlEle
     htmlElement.display = DISPLAY_INLINE
   elif htmlElement.htmlTag in BlockTags:
     htmlElement.display = DISPLAY_BLOCK
-    htmlElement.pad = true
   elif htmlElement.htmlTag in SingleTags:
     htmlElement.display = DISPLAY_SINGLE
+  elif htmlElement.htmlTag ==  tagLi:
+    htmlElement.display = DISPLAY_LIST_ITEM
   else:
     htmlElement.display = DISPLAY_NONE
 
@@ -316,25 +309,25 @@ proc getHtmlElement*(xmlElement: XmlNode, inherit: Option[HtmlElement]): HtmlEle
     if child.kind == xnText and child.text.strip().len > 0:
       htmlElement.textNodes += 1
   
-  htmlElement.rawElem = htmlElement.getRawElem()
-  htmlElement.formattedElem = htmlElement.getFormattedElem()
   return htmlElement
 
-proc getHtmlText*(text: string, parent: HtmlElement): HtmlText =
-  var textNode = HtmlText(parent: parent, text: text)
-  textNode.text = textNode.getRawText()
-  textNode.formattedText = textNode.getFormattedText()
-  return textNode
+proc getHtmlText*(parent: HtmlElement): HtmlText =
+  return HtmlText(parent: parent)
 
 proc getHtmlNode*(xmlElement: XmlNode, parent: Option[HtmlElement]): HtmlNode =
   case kind(xmlElement)
   of xnElement:
-    return HtmlNode(nodeType: NODE_ELEMENT, element: getHtmlElement(xmlElement, parent))
+    result = HtmlNode(nodeType: NODE_ELEMENT, element: getHtmlElement(xmlElement, parent))
+    result.element.node = result
   of xnText:
     assert(parent.isSome)
-    return HtmlNode(nodeType: NODE_TEXT, text: getHtmlText(xmlElement.text, parent.get()))
+    result = HtmlNode(nodeType: NODE_TEXT, text: getHtmlText(parent.get()))
+    result.rawtext = xmlElement.text
   of xnComment:
-    return HtmlNode(nodeType: NODE_COMMENT, comment: xmlElement.text)
+    result = HtmlNode(nodeType: NODE_COMMENT, comment: xmlElement.text)
   of xnCData:
-    return HtmlNode(nodeType: NODE_TEXT, text: getHtmlText(xmlElement.text, parent.get()))
+    result = HtmlNode(nodeType: NODE_TEXT, text: getHtmlText(parent.get()))
+    result.rawtext = xmlElement.text
   else: assert(false)
+  result.rawtext = result.getRawText()
+  result.fmttext = result.getFmtText()
diff --git a/main.nim b/main.nim
index bbfffd58..b0212511 100644
--- a/main.nim
+++ b/main.nim
@@ -19,7 +19,6 @@ proc loadLocalPage*(url: string): string =
 
 proc loadPageUri(uri: Uri, currentcontent: XmlNode): XmlNode =
   var moduri = uri
-  var page: XmlNode
   moduri.anchor = ""
   if uri.scheme == "" and uri.path == "" and uri.anchor != "" and currentcontent != nil:
     return currentcontent
@@ -38,11 +37,12 @@ proc main*() =
     eprint "Failed to read keymap, falling back to default"
     parseKeymap(keymapStr)
   let attrs = getTermAttributes()
-  var buffer = newBuffer(attrs)
-  var url = parseUri(paramStr(1))
+  let buffer = newBuffer(attrs)
+  let uri = parseUri(paramStr(1))
   buffers.add(buffer)
   buffer.setLocation(uri)
   buffer.htmlSource = loadPageUri(uri, buffer.htmlSource)
+  buffer.renderHtml()
   var lastUri = uri
   while displayPage(attrs, buffer):
     statusMsg("Loading...", buffer.height)
@@ -50,9 +50,9 @@ proc main*() =
     lastUri.anchor = ""
     newUri.anchor = ""
     if $lastUri != $newUri:
-      buffer.htmlSource = loadPageUri(buffer.location, buffer.htmlSource)
       buffer.clearBuffer()
-    else
+      buffer.htmlSource = loadPageUri(buffer.location, buffer.htmlSource)
+      buffer.renderHtml()
     lastUri = newUri
 
 #waitFor loadPage("https://lite.duckduckgo.com/lite/?q=hello%20world")
diff --git a/twtio.nim b/twtio.nim
index cb59686c..4d2f1083 100644
--- a/twtio.nim
+++ b/twtio.nim
@@ -9,7 +9,7 @@ template print*(s: varargs[string, `$`]) =
   for x in s:
     stdout.write(x)
 
-template eprint*(s: varargs[string, `$`]) =
+template eprint*(s: varargs[string, `$`]) = {.cast(noSideEffect).}:
   var a = false
   for x in s:
     if not a:
diff --git a/twtstr.nim b/twtstr.nim
index 5a41f3e3..983d58d8 100644
--- a/twtstr.nim
+++ b/twtstr.nim
@@ -1,21 +1,11 @@
 import terminal
 import strutils
 
-func stripNewline*(str: string): string =
-  if str.len == 0:
-    result = str
-    return
-
-  case str[^1]
-  of '\n':
-    result = str.substr(0, str.len - 2)
-  else: discard
-
 func addAnsiStyle*(str: string, style: Style): string =
   return ansiStyleCode(style) & str & "\e[0m"
 
 func addAnsiFgColor*(str: string, color: ForegroundColor): string =
-  return ansiForegroundColorCode(color) & str & "\e[0m"
+  return ansiForegroundColorCode(color) & str & ansiResetCode
 
 func maxString*(str: string, max: int): string =
   if max < str.len:
@@ -26,3 +16,6 @@ func fitValueToSize*(str: string, size: int): string =
   if str.len < size:
     return str & ' '.repeat(size - str.len)
   return str.maxString(size)
+
+func buttonStr*(str: string): string =
+  return "[".addAnsiFgColor(fgRed) & str.addAnsiFgColor(fgRed) & "]".addAnsiFgColor(fgRed)