about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2021-01-22 12:54:21 +0100
committerbptato <nincsnevem662@gmail.com>2021-01-22 12:54:21 +0100
commit5d6af7f57a89239554a9cd51fe60f8227d7ce549 (patch)
treecc89b54443b227b4fc2140ee82ceee268d989f9f
parent6e1ef74bdca4c629bda2756a6e72279e038195f4 (diff)
downloadchawan-5d6af7f57a89239554a9cd51fe60f8227d7ce549.tar.gz
failed attempt for unicode
-rw-r--r--.gitignore2
-rw-r--r--Makefile8
-rw-r--r--buffer.nim65
-rw-r--r--config.nim41
-rw-r--r--display.nim116
-rw-r--r--enums.nim66
-rw-r--r--htmlelement.nim460
-rw-r--r--keymap2
-rw-r--r--main.nim5
-rw-r--r--parser.nim17
-rw-r--r--readme.md28
-rw-r--r--twtio.nim2
-rw-r--r--twtstr.nim59
13 files changed, 531 insertions, 340 deletions
diff --git a/.gitignore b/.gitignore
index 6197c0e1..7dab9430 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
 a
 twt
+dtwt
+twt_opt
diff --git a/Makefile b/Makefile
index b0297753..5f71b0e1 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,7 @@
-all: build
-build:
-	nim compile -d:ssl -o:twt main.nim
+debug:
+	nim compile -d:ssl -o:dtwt main.nim
 release:
 	nim compile -d:release -d:ssl -o:twt main.nim
+release_opt:
+	nim compile -d:danger -d:ssl -o:twt_opt main.nim
+all: debug release release_opt
diff --git a/buffer.nim b/buffer.nim
index cc0fafc9..df1f0612 100644
--- a/buffer.nim
+++ b/buffer.nim
@@ -1,13 +1,14 @@
 import options
 import uri
 import tables
+import strutils
 
 import fusion/htmlparser/xmltree
-import fusion/htmlparser
 
 import termattrs
 import htmlelement
 import twtio
+import enums
 
 type
   Buffer* = ref BufferObj
@@ -29,19 +30,20 @@ type
     nodes*: seq[HtmlNode]
     links*: seq[HtmlNode]
     clickables*: seq[HtmlNode]
-    elements*: seq[HtmlNode]
-    idelements*: Table[string, HtmlNode]
+    elements*: seq[HtmlElement]
+    idelements*: Table[string, HtmlElement]
     selectedlink*: HtmlNode
-    location*: Uri
     printwrite*: bool
     attrs*: TermAttributes
+    document*: Document
 
 proc newBuffer*(attrs: TermAttributes): Buffer =
   return Buffer(lines: @[0],
                 rawlines: @[0],
                 width: attrs.termWidth,
                 height: attrs.termHeight,
-                cursorY: 1)
+                cursorY: 1,
+                document: newDocument())
 
 
 
@@ -50,7 +52,7 @@ func lastLine*(buffer: Buffer): int =
   return buffer.lines.len - 1
 
 func lastVisibleLine*(buffer: Buffer): int =
-  return min(buffer.fromY + buffer.height, buffer.lastLine() + 1) - 1
+  return min(buffer.fromY + buffer.height - 2, buffer.lastLine())
 
 #doesn't include newline
 func lineLength*(buffer: Buffer, line: int): int =
@@ -85,7 +87,8 @@ 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] - 2)
+  result = buffer.text.substr(buffer.lines[buffer.fromY], buffer.lines[buffer.lastVisibleLine()])
+  result.stripLineEnd()
 
 func lastNode*(buffer: Buffer): HtmlNode =
   return buffer.nodes[^1]
@@ -98,15 +101,15 @@ func onSpace*(buffer: Buffer): bool =
 
 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
+           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)
+  if buffer.selectedlink != nil and buffer.selectedLink.parentNode of HtmlElement:
+    return some(HtmlElement(buffer.selectedlink.parentNode))
   for node in buffer.nodes:
     if node.isElemNode():
       if node.getFmtLen() > 0:
-        if buffer.cursorOnNode(node): return some(node.element)
+        if buffer.cursorOnNode(node): return some(HtmlElement(node))
   return none(HtmlElement)
 
 func cursorAt*(buffer: Buffer): int =
@@ -118,7 +121,7 @@ func cursorChar*(buffer: Buffer): char =
 func canScroll*(buffer: Buffer): bool =
   return buffer.lastLine() > buffer.height
 
-func getElementById*(buffer: Buffer, id: string): HtmlNode =
+func getElementById*(buffer: Buffer, id: string): HtmlElement =
   if buffer.idelements.hasKey(id):
     return buffer.idelements[id]
   return nil
@@ -133,24 +136,26 @@ proc findSelectedNode*(buffer: Buffer): Option[HtmlNode] =
 proc addNode*(buffer: Buffer, htmlNode: HtmlNode) =
   buffer.nodes.add(htmlNode)
 
-  if htmlNode.isTextNode() and htmlNode.text.parent.islink:
+  if htmlNode.isTextNode() and htmlNode.parentElement != nil and htmlNode.parentElement.islink:
     buffer.links.add(htmlNode)
 
   if htmlNode.isElemNode():
-    case htmlNode.element.htmlTag
-    of tagInput, tagOption:
-      if not htmlNode.element.hidden:
+    case HtmlElement(htmlNode).tagType
+    of TAG_INPUT, TAG_OPTION:
+      if not HtmlElement(htmlNode).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.parentElement != nil and htmlNode.parentElement.islink:
+      let anchor = htmlNode.getParent(TAG_A)
+      assert(anchor != nil)
+      buffer.clickables.add(anchor)
 
   if htmlNode.isElemNode():
-    buffer.elements.add(htmlNode)
-    if htmlNode.element.id != "":
-      buffer.idelements[htmlNode.element.id] = htmlNode
+    let elem = HtmlElement(htmlNode)
+    buffer.elements.add(elem)
+    if elem.id != "" and not buffer.idelements.hasKey(elem.id):
+      buffer.idelements[elem.id] = elem
 
 proc writefmt*(buffer: Buffer, str: string) =
   buffer.text &= str
@@ -279,7 +284,7 @@ proc cursorNextNode*(buffer: Buffer):  bool =
   var res = buffer.cursorRight()
   if selectedNode.isNone:
     return res
-  while buffer.findSelectedNode().isSome and buffer.findSelectedNode().get() == selectedNode.get():
+  while buffer.findSelectedNode().isNone or buffer.findSelectedNode().get() == selectedNode.get():
     if buffer.cursorAtLineEnd():
       return res
     res = buffer.cursorRight()
@@ -314,7 +319,7 @@ proc cursorPrevNode*(buffer: Buffer): bool =
   var res = buffer.cursorLeft()
   if selectedNode.isNone:
     return res
-  while buffer.findSelectedNode().isSome and buffer.findSelectedNode().get() == selectedNode.get():
+  while buffer.findSelectedNode().isNone or buffer.findSelectedNode().get() == selectedNode.get():
     if buffer.cursorX == 0:
       return res
     res = res or buffer.cursorLeft()
@@ -429,26 +434,26 @@ proc checkLinkSelection*(buffer: Buffer): bool =
     if buffer.cursorOnNode(buffer.selectedlink):
       return false
     else:
-      let anchor = buffer.selectedlink.text.parent.getParent(tagA)
+      let anchor = buffer.selectedlink.getParent(TAG_A)
       anchor.selected = false
       buffer.selectedlink = nil
       buffer.hovertext = ""
   for node in buffer.links:
     if buffer.cursorOnNode(node):
       buffer.selectedlink = node
-      let anchor = node.text.parent.getParent(tagA)
+      let anchor = node.getParent(TAG_A)
       assert(anchor != nil)
       anchor.selected = true
-      buffer.hovertext = anchor.href
+      buffer.hovertext = HtmlAnchorElement(anchor).href
       return true
   return false
 
 proc gotoAnchor*(buffer: Buffer): bool =
-  if buffer.location.anchor != "":
-    let node =  buffer.getElementById(buffer.location.anchor)
+  if buffer.document.location.anchor != "":
+    let node =  buffer.getElementById(buffer.document.location.anchor)
     if node != nil:
       return buffer.scrollTo(node.y)
   return false
 
 proc setLocation*(buffer: Buffer, uri: Uri) =
-  buffer.location = buffer.location.combine(uri)
+  buffer.document.location = buffer.document.location.combine(uri)
diff --git a/config.nim b/config.nim
index ca136b9e..df3d16d6 100644
--- a/config.nim
+++ b/config.nim
@@ -88,13 +88,46 @@ proc constructActionTable*(origTable: var Table[string, TwtAction]): Table[strin
     newTable[realk] = v
   return newTable
 
-var keymapStr*: string
 macro staticReadKeymap(): untyped =
   var keymap = staticRead"keymap"
+  var normalActionMap: Table[string, TwtAction]
+  var linedActionMap: Table[string, TwtAction]
+  for line in keymap.split('\n'):
+    if line.len == 0 or line[0] == '#':
+      continue
+    let cmd = line.split(' ')
+    if cmd.len == 3:
+      if cmd[0] == "nmap":
+        normalActionMap[getRealKey(cmd[1])] = parseEnum[TwtAction](cmd[2])
+      elif cmd[0] == "lemap":
+        linedActionMap[getRealKey(cmd[1])] = parseEnum[TwtAction](cmd[2])
+
+  normalActionMap = constructActionTable(normalActionMap)
+  linedActionMap = constructActionTable(linedActionMap)
+
+  let normalActionConstr = nnkTableConstr.newTree()
+  for k, v in normalActionMap:
+    let colonExpr = nnkExprColonExpr.newTree()
+    colonExpr.add(newLit(k))
+    colonExpr.add(newLit(v))
+    normalActionConstr.add(colonExpr)
+
+  let normalActionAsgn = nnkAsgn.newTree()
+  normalActionAsgn.add(ident("normalActionRemap"))
+  normalActionAsgn.add(newCall(ident("toTable"), normalActionConstr))
+
+  let linedActionConstr = nnkTableConstr.newTree()
+  for k, v in linedActionMap:
+    let colonExpr = nnkExprColonExpr.newTree()
+    colonExpr.add(newLit(k))
+    colonExpr.add(newLit(v))
+    linedActionConstr.add(colonExpr)
 
-  let keymapLit = newLit(keymap)
-  result = quote do:
-    keymapStr = `keymapLit`
+  let linedActionAsgn = nnkAsgn.newTree()
+  linedActionAsgn.add(ident("linedActionRemap"))
+  linedActionAsgn.add(newCall(ident("toTable"), linedActionConstr))
+  result = newStmtList()
+  result.add(normalActionAsgn)
 
 staticReadKeymap()
 
diff --git a/display.nim b/display.nim
index 898993dd..5ebf78f3 100644
--- a/display.nim
+++ b/display.nim
@@ -2,9 +2,9 @@ import terminal
 import options
 import uri
 import strutils
+import unicode
 
 import fusion/htmlparser/xmltree
-import fusion/htmlparser
 
 import buffer
 import termattrs
@@ -12,6 +12,7 @@ import htmlelement
 import twtstr
 import twtio
 import config
+import enums
 
 proc clearStatusMsg*(at: int) =
   setCursorPos(0, at)
@@ -19,7 +20,7 @@ proc clearStatusMsg*(at: int) =
 
 proc statusMsg*(str: string, at: int) =
   clearStatusMsg(at)
-  print(str.addAnsiStyle(styleReverse))
+  print(str.ansiStyle(styleReverse).ansiReset())
 
 type
   RenderState = object
@@ -63,26 +64,19 @@ proc addSpaces(buffer: Buffer, state: var RenderState, n: int) =
   state.atchar += n
   state.atrawchar += n
 
-proc addSpace(buffer: Buffer, state: var RenderState) =
-  buffer.addSpaces(state, 1)
-
-proc addSpacePadding(buffer: Buffer, state: var RenderState) =
-  if not buffer.onSpace():
-    buffer.addSpace(state)
-
-proc writeWrappedText(buffer: Buffer, state: var RenderState, fmttext: string, rawtext: string) =
+proc writeWrappedText(buffer: Buffer, state: var RenderState, node: HtmlNode) =
   state.lastwidth = 0
   var n = 0
-  var fmtword = ""
-  var rawword = ""
+  var fmtword: ustring = @[]
+  var rawword: ustring = @[]
   var prevl = false
-  for c in fmttext:
-    fmtword &= c
+  for r in node.fmttext:
+    fmtword &= r
 
-    if n >= rawtext.len or rawtext[n] != c:
+    if n >= node.rawtext.len or r != node.rawtext[n]:
       continue
 
-    rawword &= c
+    rawword &= r
     state.x += 1
 
     if state.x > buffer.width:
@@ -98,22 +92,22 @@ proc writeWrappedText(buffer: Buffer, state: var RenderState, fmttext: string, r
     else:
       state.lastwidth = max(state.lastwidth, state.x)
 
-    if c == ' ':
-      buffer.writefmt(fmtword)
-      buffer.writeraw(rawword)
-      state.atchar += fmtword.len
-      state.atrawchar += rawword.len
+    if r == runeSpace:
+      buffer.writefmt($fmtword)
+      buffer.writeraw($rawword)
+      state.atchar += ($fmtword).len
+      state.atrawchar += ($rawword).len
       if prevl:
         state.x += rawword.len
         prevl = false
-      fmtword = ""
-      rawword = ""
+      fmtword = @[]
+      rawword = @[]
     n += 1
 
-  buffer.writefmt(fmtword)
-  buffer.writeraw(rawword)
-  state.atchar += fmtword.len
-  state.atrawchar += rawword.len
+  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: var RenderState) =
@@ -125,7 +119,6 @@ proc preAlignNode(buffer: Buffer, node: HtmlNode, state: var RenderState) =
     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():
@@ -140,11 +133,10 @@ proc preAlignNode(buffer: Buffer, node: HtmlNode, state: var RenderState) =
   
   if elem.display == DISPLAY_LIST_ITEM and state.indent > 0:
     var listchar = ""
-    eprint "Display", listchar, elem.parent.htmlTag
-    case elem.parent.htmlTag
-    of tagUl:
+    case elem.parentElement.tagType
+    of TAG_UL:
       listchar = "*"
-    of tagOl:
+    of TAG_OL:
       state.listval += 1
       listchar = $state.listval & ")"
     else:
@@ -174,7 +166,7 @@ proc postAlignNode(buffer: Buffer, node: HtmlNode, state: var RenderState) =
     if elem.display == DISPLAY_LIST_ITEM and node.isTextNode():
       state.indent -= 1
 
-  if elem.htmlTag == tagBr and not node.openblock:
+  if elem.tagType == TAG_BR and not node.openblock:
     buffer.flushLine(state)
 
   if elem.display == DISPLAY_LIST_ITEM and node.isElemNode():
@@ -184,16 +176,16 @@ proc renderNode(buffer: Buffer, node: HtmlNode, state: var RenderState) =
   if not node.visibleNode():
     return
   let elem = node.nodeAttr()
-  if elem.htmlTag == tagTitle:
+  if elem.tagType == TAG_TITLE:
     if node.isTextNode():
-      buffer.title = node.rawtext
+      buffer.title = $node.rawtext
     return
   else: discard
   if elem.hidden: return
 
   if not state.docenter:
     if elem.centered:
-      if not node.closeblock and elem.htmlTag != tagBr:
+      if not node.closeblock and elem.tagType != TAG_BR:
         state.centerqueue += 1
         return
     if state.centerqueue > 0:
@@ -212,8 +204,11 @@ proc renderNode(buffer: Buffer, node: HtmlNode, state: var RenderState) =
 
   node.x = state.x
   node.y = state.y
-  buffer.writeWrappedText(state, node.fmttext, node.rawtext)
-  node.width = state.lastwidth - node.x + 1
+  buffer.writeWrappedText(state, node)
+  if state.x != node.x:
+    eprint node.x, node.y, state.x, state.y, node.nodeAttr().tagType
+    eprint "len", state.atrawchar
+  node.width = state.lastwidth - node.x
   node.height = state.y - node.y + 1
 
   buffer.postAlignNode(node, state)
@@ -240,7 +235,7 @@ proc setLastHtmlLine(buffer: Buffer, state: var RenderState) =
 proc renderHtml*(buffer: Buffer) =
   var stack: seq[XmlHtmlNode]
   let first = XmlHtmlNode(xml: buffer.htmlSource,
-                         html: getHtmlNode(buffer.htmlSource, none(HtmlElement)))
+                         html: getHtmlNode(buffer.htmlSource, buffer.document))
   stack.add(first)
 
   var state = newRenderState()
@@ -248,23 +243,20 @@ proc renderHtml*(buffer: Buffer) =
     let currElem = stack.pop()
     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
+    if currElem.xml.len > 0:
+      var last = false
+      for item in currElem.xml.revItems:
+        let child = XmlHtmlNode(xml: item,
+                                html: getHtmlNode(item, currElem.html))
+        stack.add(child)
+        currElem.html.childNodes.add(child.html)
+        if not last and child.html.visibleNode():
+          last = true
+          if HtmlElement(currElem.html).display == DISPLAY_BLOCK:
+            stack[^1].html.closeblock = true
+      if last:
+        if HtmlElement(currElem.html).display == DISPLAY_BLOCK:
+          stack[^1].html.openblock = true
   buffer.setLastHtmlLine(state)
 
 proc drawHtml(buffer: Buffer) =
@@ -338,20 +330,20 @@ proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool =
     of ACTION_CLICK:
       let selectedElem = buffer.findSelectedElement()
       if selectedElem.isSome:
-        case selectedElem.get().htmlTag
-        of tagInput:
+        case selectedElem.get().tagType
+        of TAG_INPUT:
           clearStatusMsg(buffer.height)
-          let status = readLine("TEXT:", selectedElem.get().value)
+          let status = readLine("TEXT:", HtmlInputElement(selectedElem.get()).value)
           if status:
             reshape = true
             redraw = true
         else: discard
         if selectedElem.get().islink:
-          let anchor = buffer.selectedlink.text.parent.getParent(tagA).href
+          let anchor = HtmlAnchorElement(buffer.selectedlink.getParent(TAG_A)).href
           buffer.setLocation(parseUri(anchor))
           return true
     of ACTION_CHANGE_LOCATION:
-      var url = $buffer.location
+      var url = $buffer.document.location
       clearStatusMsg(buffer.height)
       let status = readLine("URL:", url)
       if status:
@@ -374,8 +366,6 @@ proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool =
     buffer.statusMsgForBuffer()
 
 proc displayPage*(attrs: TermAttributes, buffer: Buffer): bool =
-  eraseScreen()
-  termGoto(0, 0)
   #buffer.printwrite = true
   discard buffer.gotoAnchor()
   buffer.displayBuffer()
diff --git a/enums.nim b/enums.nim
new file mode 100644
index 00000000..e11059d0
--- /dev/null
+++ b/enums.nim
@@ -0,0 +1,66 @@
+type
+  NodeType* =
+    enum
+    NODE_UNKNOWN, NODE_ELEMENT, NODE_TEXT, NODE_COMMENT, NODE_CDATA, NODE_DOCUMENT
+
+  DisplayType* =
+    enum
+    DISPLAY_INLINE, DISPLAY_BLOCK, DISPLAY_SINGLE, DISPLAY_LIST_ITEM, DISPLAY_NONE
+
+  InputType* =
+    enum
+    INPUT_UNKNOWN, INPUT_BUTTON, INPUT_CHECKBOX, INPUT_COLOR, INPUT_DATE,
+    INPUT_DATETIME_LOCAL, INPUT_EMAIL, INPUT_FILE, INPUT_HIDDEN, INPUT_IMAGE,
+    INPUT_MONTH, INPUT_NUMBER, INPUT_PASSWORD, INPUT_RADIO, INPUT_RANGE,
+    INPUT_RESET, INPUT_SEARCH, INPUT_SUBMIT, INPUT_TEL, INPUT_TEXT, INPUT_TIME,
+    INPUT_URL, INPUT_WEEK
+
+  WhitespaceType* =
+    enum
+    WHITESPACE_UNKNOWN, WHITESPACE_NORMAL, WHITESPACE_NOWRAP, WHITESPACE_PRE,
+    WHITESPACE_PRE_LINE, WHITESPACE_PRE_WRAP, WHITESPACE_INITIAL,
+    WHITESPACE_INHERIT
+
+  TagType* =
+    enum
+    TAG_UNKNOWN, TAG_HTML, TAG_BASE, TAG_HEAD, TAG_LINK, TAG_META, TAG_STYLE,
+    TAG_TITLE, TAG_BODY, TAG_ADDRESS, TAG_ARTICLE, TAG_ASIDE, TAG_FOOTER,
+    TAG_HEADER, TAG_H1, TAG_H2, TAG_H3, TAG_H4, TAG_H5, TAG_H6, TAG_HGROUP,
+    TAG_MAIN, TAG_NAV, TAG_SECTION, TAG_BLOCKQUOTE, TAG_DD, TAG_DIV, TAG_DL,
+    TAG_DT, TAG_FIGCAPTION, TAG_FIGURE, TAG_HR, TAG_LI, TAG_OL, TAG_P, TAG_PRE,
+    TAG_UL, TAG_A, TAG_ABBR, TAG_B, TAG_BDI, TAG_BDO, TAG_BR, TAG_CITE,
+    TAG_CODE, TAG_DATA, TAG_DFN, TAG_EM, TAG_I, TAG_KBD, TAG_MARK, TAG_Q,
+    TAG_RB, TAG_RP, TAG_RT, TAG_RTC, TAG_RUBY, TAG_S, TAG_SAMP, TAG_SMALL,
+    TAG_SPAN, TAG_STRONG, TAG_SUB, TAG_SUP, TAG_TIME, TAG_U, TAG_VAR, TAG_WBR,
+    TAG_AREA, TAG_AUDIO, TAG_IMG, TAG_MAP, TAG_TRACK, TAG_VIDEO, TAG_EMBED,
+    TAG_IFRAME, TAG_OBJECT, TAG_PARAM, TAG_PICTURE, TAG_PORTAL, TAG_SOURCE,
+    TAG_CANVAS, TAG_NOSCRIPT, TAG_SCRIPT, TAG_DEL, TAG_INS, TAG_CAPTION,
+    TAG_COL, TAG_COLGROUP, TAG_TABLE, TAG_TBODY, TAG_TD, TAG_TFOOT, TAG_TH,
+    TAG_THEAD, TAG_TR, TAG_BUTTON, TAG_DATALIST, TAG_FIELDSET, TAG_FORM,
+    TAG_INPUT, TAG_LABEL, TAG_LEGEND, TAG_METER, TAG_OPTGROUP, TAG_OPTION,
+    TAG_OUTPUT, TAG_PROGRESS, TAG_SELECT, TAG_TEXTAREA, TAG_DETAILS,
+    TAG_DIALOG, TAG_MENU, TAG_SUMMARY, TAG_BLINK, TAG_CENTER, TAG_COMMAND,
+    TAG_CONTENT, TAG_DIR, TAG_FONT, TAG_FRAME, TAG_NOFRAMES, TAG_FRAMESET,
+    TAG_STRIKE, TAG_TT
+
+const InlineTagTypes* = {
+  TAG_A, TAG_ABBR, TAG_B, TAG_BDO, TAG_BR, TAG_BUTTON, TAG_CITE, TAG_CODE,
+  TAG_DEL, TAG_DFN, TAG_EM, TAG_FONT, TAG_I, TAG_IMG, TAG_INS, TAG_INPUT,
+  TAG_IFRAME, TAG_KBD, TAG_LABEL, TAG_MAP, TAG_OBJECT, TAG_Q, TAG_SAMP,
+  TAG_SCRIPT, TAG_SELECT, TAG_SMALL, TAG_SPAN, TAG_STRONG, TAG_SUB, TAG_SUP,
+  TAG_TEXTAREA, TAG_TT, TAG_VAR, TAG_FONT, TAG_IFRAME, TAG_U, TAG_S, TAG_STRIKE,
+  TAG_WBR
+}
+
+const BlockTagTypes* = {
+  TAG_ADDRESS, TAG_BLOCKQUOTE, TAG_CENTER, TAG_DEL, TAG_DIR, TAG_DIV, TAG_DL,
+  TAG_FIELDSET, TAG_FORM, TAG_H1, TAG_H2, TAG_H3, TAG_H4, TAG_H5, TAG_H6,
+  TAG_HR, TAG_INS, TAG_MENU, TAG_NOFRAMES, TAG_NOSCRIPT, TAG_OL, TAG_P, TAG_PRE,
+  TAG_TABLE, TAG_UL, TAG_CENTER, TAG_DIR, TAG_MENU, TAG_NOFRAMES
+}
+
+const SingleTagTypes* = {
+  TAG_AREA, TAG_BASE, TAG_BR, TAG_COL, TAG_EMBED, TAG_FRAME, TAG_HR, TAG_IMG,
+  TAG_INPUT, TAG_SOURCE, TAG_TRACK, TAG_LINK, TAG_META, TAG_PARAM, TAG_WBR,
+  TAG_COMMAND
+}
diff --git a/htmlelement.nim b/htmlelement.nim
index 35430f1e..c37b294e 100644
--- a/htmlelement.nim
+++ b/htmlelement.nim
@@ -1,49 +1,51 @@
 import strutils
-import re
 import terminal
-import options
+import uri
+import unicode
 
 import fusion/htmlparser
 import fusion/htmlparser/xmltree
 
 import twtstr
 import twtio
+import enums
+import macros
 
 type
-  NodeType* =
-    enum
-    NODE_ELEMENT, NODE_TEXT, NODE_COMMENT
-  DisplayType* =
-    enum
-    DISPLAY_INLINE, DISPLAY_BLOCK, DISPLAY_SINGLE, DISPLAY_LIST_ITEM, DISPLAY_NONE
-  InputType* =
-    enum
-    INPUT_BUTTON, INPUT_CHECKBOX, INPUT_COLOR, INPUT_DATE, INPUT_DATETIME_LOCAL,
-    INPUT_EMAIL, INPUT_FILE, INPUT_HIDDEN, INPUT_IMAGE, INPUT_MONTH,
-    INPUT_NUMBER, INPUT_PASSWORD, INPUT_RADIO, INPUT_RANGE, INPUT_RESET,
-    INPUT_SEARCH, INPUT_SUBMIT, INPUT_TEL, INPUT_TEXT, INPUT_TIME, INPUT_URL,
-    INPUT_WEEK, INPUT_UNKNOWN
-  WhitespaceType* =
-    enum
-    WHITESPACE_NORMAL, WHITESPACE_NOWRAP,
-    WHITESPACE_PRE, WHITESPACE_PRE_LINE, WHITESPACE_PRE_WRAP,
-    WHITESPACE_INITIAL, WHITESPACE_INHERIT
+  HtmlNode* = ref HtmlNodeObj
+  HtmlNodeObj = object of RootObj
+    nodeType*: NodeType
+    childNodes*: seq[HtmlNode]
+    firstChild*: HtmlNode
+    isConnected*: bool
+    lastChild*: HtmlNode
+    nextSibling*: HtmlNode
+    previousSibling*: HtmlNode
+    parentNode*: HtmlNode
+    parentElement*: HtmlElement
+
+    rawtext*: ustring
+    fmttext*: ustring
+    x*: int
+    y*: int
+    width*: int
+    height*: int
+    openblock*: bool
+    closeblock*: bool
+    hidden*: bool
+
+  Document* = ref DocumentObj
+  DocumentObj = object of HtmlNodeObj
+    location*: Uri
 
-type
-  HtmlText* = ref HtmlTextObj
-  HtmlTextObj = object
-    parent*: HtmlElement
   HtmlElement* = ref HtmlElementObj
-  HtmlElementObj = object
-    node*: HtmlNode
+  HtmlElementObj = object of HtmlNodeObj
     id*: string
+    tagType*: TagType
     name*: string
-    value*: string
     centered*: bool
-    hidden*: bool
     display*: DisplayType
     innerText*: string
-    textNodes*: int
     margintop*: int
     marginbottom*: int
     marginleft*: int
@@ -53,40 +55,55 @@ type
     italic*: bool
     underscore*: bool
     islink*: bool
-    parent*: HtmlElement
-    case htmlTag*: HtmlTag
-    of tagInput:
-      itype*: InputType
-      size*: int
-    of tagA:
-      href*: string
-      selected*: bool
-    else:
-      discard
-  HtmlNode* = ref HtmlNodeObj
-  HtmlNodeObj = object
-    case nodeType*: NodeType
-    of NODE_ELEMENT:
-      element*: HtmlElement
-    of NODE_TEXT:
-      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
+    selected*: bool
+
+  HtmlInputElement* = ref HtmlInputElementObj
+  HtmlInputElementObj = object of HtmlElementObj
+    itype*: InputType
+    autofocus*: bool
+    required*: bool
+    value*: string
+    size*: int
+
+  HtmlAnchorElement* = ref HtmlAnchorElementObj
+  HtmlAnchorElementObj = object of HtmlElementObj
+    href*: string
+
+  HtmlSelectElement* = ref HtmlSelectElementObj
+  HtmlSelectElementObj = object of HtmlElementObj
+    value*: string
+
+  HtmlOptionElement* = ref HtmlOptionElementObj
+  HtmlOptionElementObj = object of HtmlElementObj
+    value*: string
+
+#no I won't manually write all this down
+#maybe todo to accept stuff other than tagtype (idk how useful that'd be)
+macro genEnumCase(s: string): untyped =
+  let casestmt = nnkCaseStmt.newTree() 
+  casestmt.add(ident("s"))
+  for i in low(TagType) .. high(TagType):
+    let ret = nnkReturnStmt.newTree()
+    ret.add(newLit(TagType(i)))
+    let branch = nnkOfBranch.newTree()
+    let enumname = $TagType(i)
+    let tagname = enumname.substr("TAG_".len, enumname.len - 1).tolower()
+    branch.add(newLit(tagname))
+    branch.add(ret)
+    casestmt.add(branch)
+  let ret = nnkReturnStmt.newTree()
+  ret.add(newLit(TAG_UNKNOWN))
+  let branch = nnkElse.newTree()
+  branch.add(ret)
+  casestmt.add(branch)
+
+func tagType*(s: string): TagType =
+  genEnumCase(s)
 
 func nodeAttr*(node: HtmlNode): HtmlElement =
   case node.nodeType
-  of NODE_TEXT: return node.text.parent
-  of NODE_ELEMENT: return node.element
+  of NODE_TEXT: return node.parentElement
+  of NODE_ELEMENT: return HtmlElement(node)
   else: assert(false)
 
 func displayed*(node: HtmlNode): bool =
@@ -98,6 +115,15 @@ func isTextNode*(node: HtmlNode): bool =
 func isElemNode*(node: HtmlNode): bool =
   return node.nodeType == NODE_ELEMENT
 
+func isComment*(node: HtmlNode): bool =
+  return node.nodeType == NODE_COMMENT
+
+func isCData*(node: HtmlNode): bool =
+  return node.nodeType == NODE_CDATA
+
+func isDocument*(node: HtmlNode): bool =
+  return node.nodeType == NODE_DOCUMENT
+
 func getFmtLen*(htmlNode: HtmlNode): int =
   return htmlNode.fmttext.len
 
@@ -139,195 +165,197 @@ func toInputType*(str: string): InputType =
 func toInputSize*(str: string): int =
   if str.len == 0:
     return 20
+  for c in str:
+    if not c.isDigit:
+      return 20
   return str.parseInt()
 
-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")
-  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":
-        htmlElement.value = item.attr("value")
-        break
-  htmlElement.name = xmlElement.attr("name")
-  return htmlElement
-
-func getOptionElement(xmlElement: XmlNode, htmlElement: HtmlElement): HtmlElement =
-  assert(htmlElement.htmlTag == tagOption)
-  htmlElement.value = xmlElement.attr("value")
-  if htmlElement.parent.value != htmlElement.value:
-    htmlElement.hidden = true
-  return htmlElement
-
-func getFormattedInput(htmlElement: HtmlElement): string =
-  case htmlElement.itype
+func getFmtInput(inputElement: HtmlInputElement): ustring =
+  case inputElement.itype
   of INPUT_TEXT, INPUT_SEARCH:
-    let valueFit = fitValueToSize(htmlElement.value, htmlElement.size)
-    return valueFit.addAnsiStyle(styleUnderscore).buttonStr()
+    let valueFit = fitValueToSize(inputElement.value.toRunes(), inputElement.size)
+    return valueFit.ansiStyle(styleUnderscore).ansiReset().buttonFmt()
   of INPUT_SUBMIT:
-    return htmlElement.value.buttonStr()
+    return inputElement.value.toRunes().buttonFmt()
   else: discard
 
-func getRawInput(htmlElement: HtmlElement): string =
-  case htmlElement.itype
+func getRawInput(inputElement: HtmlInputElement): ustring =
+  case inputElement.itype
   of INPUT_TEXT, INPUT_SEARCH:
-    return "[" & htmlElement.value.fitValueToSize(htmlElement.size) & "]"
+    return inputElement.value.toRunes().fitValueToSize(inputElement.size).buttonFmt()
   of INPUT_SUBMIT:
-    return "[" & htmlElement.value & "]"
+    return inputElement.value.toRunes().buttonFmt()
   else: discard
 
-func getParent*(htmlElement: HtmlElement, htmlTag: HtmlTag): HtmlElement =
-  result = htmlElement
-  while result != nil and result.htmlTag != htmlTag:
-    result = result.parent
+func getParent*(htmlNode: HtmlNode, tagType: TagType): HtmlElement =
+  var pnode = htmlNode.parentElement
+  while pnode != nil and pnode.tagType != tagType:
+    pnode = pnode.parentElement
+  
+  return pnode
 
-func getRawText*(htmlNode: HtmlNode): string =
+proc getRawText*(htmlNode: HtmlNode): ustring =
   if htmlNode.isElemNode():
-    case htmlNode.element.htmlTag
-    of tagInput: return htmlNode.element.getRawInput()
-    else: return ""
+    case HtmlElement(htmlNode).tagType
+    of TAG_INPUT: return HtmlInputElement(htmlNode).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.parentElement != nil and htmlNode.parentElement.tagType != TAG_PRE:
+      result = htmlNode.rawtext.remove(runeNewline)
+      if unicode.strip($result).toRunes().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)
+          if htmlNode.previousSibling == nil or htmlNode.previousSibling.nodeAttr().display != DISPLAY_INLINE:
+            result = unicode.strip($result, true, false).toRunes()
+          if htmlNode.nextSibling == nil or htmlNode.nextSibling.nodeAttr().display != DISPLAY_INLINE:
+            result = unicode.strip($result, false, true).toRunes()
       else:
-        result = ""
+        result = @[]
     else:
-      result = htmlNode.rawtext.strip()
-    if htmlNode.text.parent.htmlTag == tagOption:
-      result = "[" & result & "]"
+      result = unicode.strip($htmlNode.rawtext).toRunes()
+    if htmlNode.parentElement != nil and htmlNode.parentElement.tagType == TAG_OPTION:
+      result = result.buttonRaw()
   else:
     assert(false)
 
-func getFmtText*(htmlNode: HtmlNode): string =
+func getFmtText*(htmlNode: HtmlNode): ustring =
   if htmlNode.isElemNode():
-    case htmlNode.element.htmlTag
-    of tagInput: return htmlNode.element.getFormattedInput()
-    else: return ""
+    case HtmlElement(htmlNode).tagType
+    of TAG_INPUT: return HtmlInputElement(htmlNode).getFmtInput()
+    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 htmlNode.parentElement != nil and htmlNode.parentElement.islink:
+      result = result.ansiFgColor(fgBlue)
+      let parent = HtmlElement(htmlNode.parentNode).getParent(TAG_A)
       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)
-
-proc newElemFromParent(elem: HtmlElement, parentOpt: Option[HtmlElement]): HtmlElement =
-  if parentOpt.isSome:
-    let parent = parentOpt.get()
-    elem.centered = parent.centered
-    elem.bold = parent.bold
-    elem.italic = parent.italic
-    elem.underscore = parent.underscore
-    elem.hidden = parent.hidden
-    elem.display = parent.display
-    #elem.margin = parent.margin
-    #elem.margintop = parent.margintop
-    #elem.marginbottom = parent.marginbottom
-    #elem.marginleft = parent.marginleft
-    #elem.marginright = parent.marginright
-    elem.parent = parent
-    elem.islink = parent.islink
-
-  return elem
-
-proc getHtmlElement*(xmlElement: XmlNode, inherit: Option[HtmlElement]): HtmlElement =
-  assert kind(xmlElement) == xnElement
-  var htmlElement: HtmlElement
-  htmlElement = newElemFromParent(HtmlElement(htmlTag: htmlTag(xmlElement)), inherit)
-  htmlElement.id = xmlElement.attr("id")
-
-  if htmlElement.htmlTag in InlineTags:
-    htmlElement.display = DISPLAY_INLINE
-  elif htmlElement.htmlTag in BlockTags:
-    htmlElement.display = DISPLAY_BLOCK
-  elif htmlElement.htmlTag in SingleTags:
-    htmlElement.display = DISPLAY_SINGLE
-  elif htmlElement.htmlTag ==  tagLi:
-    htmlElement.display = DISPLAY_LIST_ITEM
+        result = result.ansiStyle(styleUnderscore).ansiReset()
+
+    if HtmlElement(htmlNode.parentNode).tagType == TAG_OPTION:
+      result = result.ansiFgColor(fgRed).ansiReset()
+
+    if HtmlElement(htmlNode.parentNode).bold:
+      result = result.ansiStyle(styleBright).ansiReset()
+    if HtmlElement(htmlNode.parentNode).italic:
+      result = result.ansiStyle(styleItalic).ansiReset()
+    if HtmlElement(htmlNode.parentNode).underscore:
+      result = result.ansiStyle(styleUnderscore).ansiReset()
   else:
-    htmlElement.display = DISPLAY_NONE
-
-  case htmlElement.htmlTag
-  of tagCenter:
-    htmlElement.centered = true
-  of tagB:
-    htmlElement.bold = true
-  of tagI:
-    htmlElement.italic = true
-  of tagU:
-    htmlElement.underscore = true
-  of tagHead:
-    htmlElement.hidden = true
-  of tagStyle:
-    htmlElement.hidden = true
-  of tagScript:
-    htmlElement.hidden = true
-  of tagInput:
-    htmlElement = getInputElement(xmlElement, htmlElement)
-  of tagA:
-    htmlElement = getAnchorElement(xmlElement, htmlElement)
-  of tagSelect:
-    htmlElement = getSelectElement(xmlElement, htmlElement)
-  of tagOption:
-    htmlElement = getOptionElement(xmlElement, htmlElement)
-  of tagPre, tagTd, tagTh:
-    htmlElement.margin = 1
+    assert(false)
+
+proc getHtmlElement*(xmlElement: XmlNode, parentNode: HtmlNode): HtmlElement =
+  assert kind(xmlElement) == xnElement
+  let tagType = xmlElement.tag().tagType()
+
+  case tagType
+  of TAG_INPUT: result = new(HtmlInputElement)
+  of TAG_A: result = new(HtmlAnchorElement)
+  else: new(result)
+
+  result.tagType = tagType
+  result.parentNode = parentNode
+  if parentNode.isElemNode():
+    result.parentElement = HtmlElement(parentNode)
+
+  result.id = xmlElement.attr("id")
+
+  if tagType in InlineTagTypes:
+    result.display = DISPLAY_INLINE
+  elif tagType in BlockTagTypes:
+    result.display = DISPLAY_BLOCK
+  elif tagType in SingleTagTypes:
+    result.display = DISPLAY_SINGLE
+  elif tagType ==  TAG_LI:
+    result.display = DISPLAY_LIST_ITEM
   else:
-    discard
+    result.display = DISPLAY_NONE
+
+  case tagType
+  of TAG_CENTER:
+    result.centered = true
+  of TAG_B:
+    result.bold = true
+  of TAG_I:
+    result.italic = true
+  of TAG_U:
+    result.underscore = true
+  of TAG_HEAD:
+    result.hidden = true
+  of TAG_STYLE:
+    result.hidden = true
+  of TAG_SCRIPT:
+    result.hidden = true
+  of TAG_INPUT:
+    let inputElement = HtmlInputElement(result)
+    inputElement.itype = xmlElement.attr("type").toInputType()
+    if inputElement.itype == INPUT_HIDDEN:
+      inputElement.hidden = true
+    inputElement.size = xmlElement.attr("size").toInputSize()
+    inputElement.value = xmlElement.attr("value")
+    result = inputElement
+  of TAG_A:
+    let anchorElement = HtmlAnchorElement(result)
+    anchorElement.href = xmlElement.attr("href")
+    anchorElement.islink = true
+    result = anchorElement
+  of TAG_SELECT:
+    var selectElement = new(HtmlSelectElement)
+    for item in xmlElement.items:
+      if item.kind == xnElement:
+        if item.tag == "option":
+          selectElement.value = item.attr("value")
+          break
+    selectElement.name = xmlElement.attr("name")
+    result = selectElement
+  of TAG_OPTION:
+    var optionElement = new(HtmlOptionElement)
+    optionElement.value = xmlElement.attr("value")
+    if parentNode.isElemNode() and HtmlSelectElement(parentNode).value != optionElement.value:
+      optionElement.hidden = true
+    result = optionElement
+  of TAG_PRE, TAG_TD, TAG_TH:
+    result.margin = 1
+  else: discard
 
-  for child in xmlElement.items:
-    if child.kind == xnText and child.text.strip().len > 0:
-      htmlElement.textNodes += 1
+  if parentNode.isElemNode():
+    let parent = HtmlElement(parentNode)
+    result.centered = result.centered or parent.centered
+    result.bold = result.bold or parent.bold
+    result.italic = result.italic or parent.italic
+    result.underscore = result.underscore or parent.underscore
+    result.hidden = result.hidden or parent.hidden
+    result.islink = result.islink or parent.islink
   
-  return htmlElement
-
-proc getHtmlText*(parent: HtmlElement): HtmlText =
-  return HtmlText(parent: parent)
 
-proc getHtmlNode*(xmlElement: XmlNode, parent: Option[HtmlElement]): HtmlNode =
+proc getHtmlNode*(xmlElement: XmlNode, parent: HtmlNode): HtmlNode =
   case kind(xmlElement)
   of xnElement:
-    result = HtmlNode(nodeType: NODE_ELEMENT, element: getHtmlElement(xmlElement, parent))
-    result.element.node = result
+    result = getHtmlElement(xmlElement, parent)
+    result.nodeType = NODE_ELEMENT
   of xnText:
-    assert(parent.isSome)
-    result = HtmlNode(nodeType: NODE_TEXT, text: getHtmlText(parent.get()))
-    result.rawtext = xmlElement.text
+    new(result)
+    result.nodeType = NODE_TEXT
+    result.rawtext = xmlElement.text.toRunes()
   of xnComment:
-    result = HtmlNode(nodeType: NODE_COMMENT, comment: xmlElement.text)
+    new(result)
+    result.nodeType = NODE_COMMENT
+    result.rawtext = xmlElement.text.toRunes()
   of xnCData:
-    result = HtmlNode(nodeType: NODE_TEXT, text: getHtmlText(parent.get()))
-    result.rawtext = xmlElement.text
+    new(result)
+    result.nodeType = NODE_CDATA
+    result.rawtext = xmlElement.text.toRunes()
   else: assert(false)
+
+  result.parentNode = parent
+  if parent.isElemNode():
+    result.parentElement = HtmlElement(parent)
+
   result.rawtext = result.getRawText()
   result.fmttext = result.getFmtText()
+  if parent.childNodes.len > 0:
+    result.previousSibling = parent.childNodes[^1]
+    result.previousSibling.nextSibling = result
+  parent.childNodes.add(result)
+
+func newDocument*(): Document =
+  new(result)
+  result.nodeType = NODE_DOCUMENT
diff --git a/keymap b/keymap
index 218cabd3..b63600f7 100644
--- a/keymap
+++ b/keymap
@@ -27,6 +27,8 @@ nmap \e[6~ ACTION_PAGE_DOWN
 nmap \e[5~ ACTION_PAGE_UP
 nmap C-e ACTION_SCROLL_DOWN
 nmap C-y ACTION_SCROLL_UP
+nmap J ACTION_SCROLL_DOWN
+nmap K ACTION_SCROLL_UP
 nmap C-m ACTION_CLICK
 nmap C-j ACTION_CLICK
 nmap C-l ACTION_CHANGE_LOCATION
diff --git a/main.nim b/main.nim
index b0212511..c98f0db5 100644
--- a/main.nim
+++ b/main.nim
@@ -35,7 +35,6 @@ proc main*() =
     quit(1)
   if not readKeymap("keymap"):
     eprint "Failed to read keymap, falling back to default"
-    parseKeymap(keymapStr)
   let attrs = getTermAttributes()
   let buffer = newBuffer(attrs)
   let uri = parseUri(paramStr(1))
@@ -46,12 +45,12 @@ proc main*() =
   var lastUri = uri
   while displayPage(attrs, buffer):
     statusMsg("Loading...", buffer.height)
-    var newUri = buffer.location
+    var newUri = buffer.document.location
     lastUri.anchor = ""
     newUri.anchor = ""
     if $lastUri != $newUri:
       buffer.clearBuffer()
-      buffer.htmlSource = loadPageUri(buffer.location, buffer.htmlSource)
+      buffer.htmlSource = loadPageUri(buffer.document.location, buffer.htmlSource)
       buffer.renderHtml()
     lastUri = newUri
 
diff --git a/parser.nim b/parser.nim
new file mode 100644
index 00000000..591a43e6
--- /dev/null
+++ b/parser.nim
@@ -0,0 +1,17 @@
+import parsexml
+import htmlelement
+import streams
+
+func parseNextNode(str: string) =
+  return
+
+var s = ""
+proc parseHtml*(inputStream: Stream) =
+  var x: XmlParser
+  x.open(inputStream, "")
+  while true:
+    x.next()
+    case x.kind
+    of xmlElementStart: discard
+    of xmlEof: break
+    else: discard
diff --git a/readme.md b/readme.md
index 62c97ce6..fe58b84d 100644
--- a/readme.md
+++ b/readme.md
@@ -1,27 +1,35 @@
 # twt - a web browser in your terminal
 
 ## What is this?
-A terminal web browser. It displays websites in your terminal.
+A terminal web browser. It displays websites in your terminal and allows you to navigate on them.
 
 ## Why make another web browser?
-I've found other terminal web browsers insufficient for my needs. In fact, I started working on this after failing to add JavaScript support to w3m.
+I've found other terminal web browsers insufficient for my needs, so I thought it'd be a fun excercise to write one myself.  
+I don't really want a standard-compliant browser, or one that displays pages perfectly - the only way you could do that in a terminal is to work like browsh, which kinda defeats the point of a terminal web browser. I want one that is good enough for daily use - something like lynx or w3m, but better.  
+So the aim is to implement HTML rendering, some degree of JS support, and a very limited subset of CSS. Plus some other things I'd add to w3m if it weren't 50k lines of incomprehensible ancient C code.
 
 ## So what can this do?
 Currently implemented features are:
-* basic html rendering (WIP)
+
+* basic html rendering (very much WIP)
 * custom keybindings
 
-Planned features:
-* image (sixel/kitty)
-* video (sixel/kitty)
-* audio
+Planned features (roughly in order of importance):
+
+* improved html rendering and parsing
+* form
 * table
 * cookie
-* form
-* JavaScript
 * SOCKS proxy
+* HTTP proxy
+* image (sixel/kitty)
+* audio
+* JavaScript
 * extension API (adblock support?)
-* markdown? (with pandoc?)
+* video (sixel/kitty)
+* custom charsets?
+* async?
+* markdown? (with pandoc or built-in parser?)
 * gopher?
 * gemini?
 
diff --git a/twtio.nim b/twtio.nim
index 4d2f1083..4c02cfb0 100644
--- a/twtio.nim
+++ b/twtio.nim
@@ -65,7 +65,7 @@ proc readLine*(prompt: string, current: var string): bool =
         print('\b'.repeat(new.len - cursor))
     of ACTION_LINED_ESC:
       new &= c
-      print("^[".addAnsiFgColor(fgBlue).addAnsiStyle(styleBright))
+      print("^[".ansiFgColor(fgBlue).ansiStyle(styleBright).ansiReset())
     of ACTION_LINED_CLEAR:
       print(' '.repeat(new.len - cursor + 1))
       print('\b'.repeat(new.len - cursor + 1))
diff --git a/twtstr.nim b/twtstr.nim
index 983d58d8..50f0e4da 100644
--- a/twtstr.nim
+++ b/twtstr.nim
@@ -1,21 +1,60 @@
 import terminal
 import strutils
+import unicode
 
-func addAnsiStyle*(str: string, style: Style): string =
-  return ansiStyleCode(style) & str & "\e[0m"
+type ustring* = seq[Rune]
 
-func addAnsiFgColor*(str: string, color: ForegroundColor): string =
-  return ansiForegroundColorCode(color) & str & ansiResetCode
+const runeSpace*: Rune = " ".toRunes()[0]
+const runeNewline*: Rune = "\n".toRunes()[0]
+const runeReturn*: Rune = "\r".toRunes()[0]
+
+func isWhitespace(r: Rune): bool =
+  case r
+  of runeSpace, runeNewline, runeReturn: return true
+  else: return false
+
+func ansiStyle*(str: string, style: Style): string =
+  return ansiStyleCode(style) & str
+
+func ansiFgColor*(str: string, color: ForegroundColor): string =
+  return ansiForegroundColorCode(color) & str
+
+func ansiReset*(str: string): string =
+  return str & ansiResetCode
+
+func ansiStyle*(str: ustring, style: Style): ustring =
+  return ansiStyleCode(style).toRunes() & str
+
+func ansiFgColor*(str: ustring, color: ForegroundColor): ustring =
+  return ansiForegroundColorCode(color).toRunes & str
+
+func ansiReset*(str: ustring): ustring =
+  return str & ansiResetCode.toRunes()
+
+func maxString*(str: ustring, max: int): ustring =
+  result = str
+  if max < str.len:
+    result.setLen(max - 2)
+    result &= "$".toRunes()
 
 func maxString*(str: string, max: int): string =
+  result = str
   if max < str.len:
-    return str.substr(0, max - 2) & "$"
-  return str
+    result.setLen(max - 1)
+    result[max - 2] = '$'
 
-func fitValueToSize*(str: string, size: int): string =
+func fitValueToSize*(str: ustring, size: int): ustring =
   if str.len < size:
-    return str & ' '.repeat(size - str.len)
+    return str & ' '.repeat(size - str.len).toRunes()
   return str.maxString(size)
 
-func buttonStr*(str: string): string =
-  return "[".addAnsiFgColor(fgRed) & str.addAnsiFgColor(fgRed) & "]".addAnsiFgColor(fgRed)
+func buttonFmt*(str: ustring): ustring =
+  return "[".toRunes().ansiFgColor(fgRed).ansiReset() & str.ansiFgColor(fgRed).ansiReset() & "]".ansiFgColor(fgRed).toRunes().ansiReset()
+
+func buttonRaw*(str: ustring): ustring = 
+  return "[".toRunes() & str & "]".toRunes()
+
+func remove*(s: ustring, r: Rune): ustring =
+  for c in s:
+    if c != r:
+      result.add(c)