about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--src/bindings/quickjs.nim88
-rw-r--r--src/config/config.nim1
-rw-r--r--src/css/match.nim10
-rw-r--r--src/display/client.nim184
-rw-r--r--src/html/dom.nim75
-rw-r--r--src/html/htmlparser.nim22
-rw-r--r--src/html/htmltokenizer.nim2
-rw-r--r--src/io/lineedit.nim4
-rw-r--r--src/io/loader.nim1
-rw-r--r--src/io/request.nim50
-rw-r--r--src/js/javascript.nim294
-rw-r--r--src/main.nim8
-rw-r--r--src/types/url.nim49
-rw-r--r--src/utils/twtstr.nim81
14 files changed, 638 insertions, 231 deletions
diff --git a/src/bindings/quickjs.nim b/src/bindings/quickjs.nim
index bdd156e2..4a1ad639 100644
--- a/src/bindings/quickjs.nim
+++ b/src/bindings/quickjs.nim
@@ -23,11 +23,43 @@ when hlib != "":
   {.passC: "-I" & hlib.}
 {.passL: "-lquickjs -lm -lpthread".}
 
+const                         ##  all tags with a reference count are negative
+  JS_TAG_FIRST* = -10           ##  first negative tag
+  JS_TAG_BIG_INT* = -10
+  JS_TAG_BIG_FLOAT* = -9
+  JS_TAG_SYMBOL* = -8
+  JS_TAG_STRING* = -7
+  JS_TAG_SHAPE* = -6            ##  used internally during GC
+  JS_TAG_ASYNC_FUNCTION* = -5   ##  used internally during GC
+  JS_TAG_VAR_REF* = -4          ##  used internally during GC
+  JS_TAG_MODULE* = -3           ##  used internally
+  JS_TAG_FUNCTION_BYTECODE* = -2 ##  used internally
+  JS_TAG_OBJECT* = -1
+  JS_TAG_INT* = 0
+  JS_TAG_BOOL* = 1
+  JS_TAG_NULL* = 2
+  JS_TAG_UNDEFINED* = 3
+  JS_TAG_UNINITIALIZED* = 4
+  JS_TAG_CATCH_OFFSET* = 5
+  JS_TAG_EXCEPTION* = 6
+  JS_TAG_FLOAT64* = 7           ##  any larger tag is FLOAT64 if JS_NAN_BOXING
+
 when sizeof(int) < sizeof(int64):
   {.passC: "-DJS_NAN_BOXING".}
   type
     JSValue* {.importc, header: qjsheader.} = uint64
-  # uh this won't compile you're on your own
+
+  template JS_VALUE_GET_TAG*(v: untyped): int32 =
+    cast[int32](v shr 32)
+
+  template JS_VALUE_GET_PTR*(v: untyped): pointer =
+    cast[pointer](v)
+
+  template JS_MKVAL*(t, val: untyped): JSValue =
+    JSValue((uint64(t) shl 32) or uint32(val))
+
+  template JS_MKPTR*(t, p: untyped): JSValue =
+    JSValue((cast[uint64](t) shl 32) or cast[uint](p))
 else:
   type
     JSValueUnion* {.importc, header: qjsheader, union.} = object
@@ -38,6 +70,18 @@ else:
       u*: JSValueUnion
       tag*: int64
 
+  template JS_VALUE_GET_TAG*(v: untyped): int32 =
+    cast[int32](v.tag)
+
+  template JS_VALUE_GET_PTR*(v: untyped): pointer =
+    cast[pointer](v.u)
+
+  template JS_MKVAL*(t, val: untyped): JSValue =
+    JSValue(u: JSValueUnion(`int32`: val), tag: t)
+
+  template JS_MKPTR*(t, p: untyped): JSValue =
+    JSValue(u: JSValueUnion(`ptr`: p), tag: t)
+
 type
   JSRuntime* = ptr object
   JSContext* = ptr object
@@ -136,39 +180,6 @@ converter toBool*(js: JS_BOOl): bool {.inline.} =
 converter toJSBool*(b: bool): JS_BOOL {.inline.} =
   cast[JS_BOOL](cint(b))
 
-const                         ##  all tags with a reference count are negative
-  JS_TAG_FIRST* = -10           ##  first negative tag
-  JS_TAG_BIG_INT* = -10
-  JS_TAG_BIG_FLOAT* = -9
-  JS_TAG_SYMBOL* = -8
-  JS_TAG_STRING* = -7
-  JS_TAG_SHAPE* = -6            ##  used internally during GC
-  JS_TAG_ASYNC_FUNCTION* = -5   ##  used internally during GC
-  JS_TAG_VAR_REF* = -4          ##  used internally during GC
-  JS_TAG_MODULE* = -3           ##  used internally
-  JS_TAG_FUNCTION_BYTECODE* = -2 ##  used internally
-  JS_TAG_OBJECT* = -1
-  JS_TAG_INT* = 0
-  JS_TAG_BOOL* = 1
-  JS_TAG_NULL* = 2
-  JS_TAG_UNDEFINED* = 3
-  JS_TAG_UNINITIALIZED* = 4
-  JS_TAG_CATCH_OFFSET* = 5
-  JS_TAG_EXCEPTION* = 6
-  JS_TAG_FLOAT64* = 7           ##  any larger tag is FLOAT64 if JS_NAN_BOXING
-
-template JS_MKVAL*(t, val: untyped): JSValue =
-  JSValue(u: JSValueUnion(`int32`: val), tag: t)
-
-template JS_MKPTR*(t, p: untyped): JSValue =
-  JSValue(u: JSValueUnion(`ptr`: p), tag: t)
-
-template JS_VALUE_GET_TAG*(v: untyped): int =
-  cast[int32](v.tag)
-
-template JS_VALUE_GET_PTR*(v: untyped): pointer =
-  cast[pointer](v.u)
-
 const
   JS_NULL* = JS_MKVAL(JS_TAG_NULL, 0)
   JS_UNDEFINED* = JS_MKVAL(JS_TAG_UNDEFINED, 0)
@@ -211,6 +222,7 @@ const
   JS_PROP_GETSET* = (1 shl 4)
   JS_PROP_VARREF* = (2 shl 4) # used internally
   JS_PROP_AUTOINIT* = (3 shl 4) # used internally
+  JS_PROP_THROW* = (1 shl 14)
 
 const
   JS_GPN_STRING_MASK* = (1 shl 0)
@@ -219,7 +231,6 @@ const
   JS_GPN_ENUM_ONLY* = (1 shl 3)
   JS_GPN_SET_ENUM* = (1 shl 4)
 
-
 template JS_CFUNC_DEF*(n: string, len: uint8, func1: JSCFunction): JSCFunctionListEntry = 
   JSCFunctionListEntry(name: cstring(n),
                        prop_flags: JS_PROP_WRITABLE or JS_PROP_CONFIGURABLE,
@@ -237,6 +248,7 @@ template JS_CGETSET_DEF*(n: string, fgetter, fsetter: untyped): JSCFunctionListE
                          getset: JSCFunctionListEntryGetSet(get: JSCFunctionType(getter: fgetter),
                                                             set: JSCFunctionType(setter: fsetter))))
 
+
 {.push header: qjsheader, importc, cdecl.}
 
 proc JS_NewRuntime*(): JSRuntime
@@ -252,9 +264,11 @@ proc JS_FreeContext*(ctx: JSContext)
 proc JS_GetGlobalObject*(ctx: JSContext): JSValue
 proc JS_IsInstanceOf*(ctx: JSContext, val: JSValue, obj: JSValue): cint
 
+proc JS_NewArray*(ctx: JSContext): JSValue
 proc JS_NewObject*(ctx: JSContext): JSValue
 proc JS_NewObjectClass*(ctx: JSContext, class_id: JSClassID): JSValue
 proc JS_NewObjectProto*(ctx: JSContext, proto: JSValue): JSValue
+proc JS_NewObjectProtoClass*(ctx: JSContext, proto: JSValue, class_id: JSClassID): JSValue
 proc JS_SetOpaque*(obj: JSValue, opaque: pointer)
 proc JS_GetOpaque*(obj: JSValue, class_id: JSClassID): pointer
 proc JS_GetOpaque2*(ctx: JSContext, obj: JSValue, class_id: JSClassID): pointer
@@ -288,6 +302,9 @@ proc JS_NewCFunction*(ctx: JSContext, cfunc: JSCFunction, name: cstring, length:
 
 proc JS_NewString*(ctx: JSContext, str: cstring): JSValue
 
+proc JS_SetProperty*(ctx: JSContext, this_obj: JSValue, prop: JSAtom, val: JSValue): cint
+proc JS_SetPropertyUint32*(ctx: JSContext, this_obj: JSValue, idx: uint32, val: JSValue): cint
+proc JS_SetPropertyInt64*(ctx: JSContext, this_obj: JSValue, idx: int64, val: JSValue): cint
 proc JS_SetPropertyStr*(ctx: JSContext, this_obj: JSValue, prop: cstring, val: JSValue): cint
 proc JS_SetPropertyFunctionList*(ctx: JSContext, obj: JSValue, tab: ptr JSCFunctionListEntry, len: cint)
 proc JS_GetProperty*(ctx: JSContext, this_obj: JSValue, prop: JSAtom): JSValue
@@ -300,6 +317,7 @@ proc JS_Call*(ctx: JSContext, func_obj, this_obj: JSValue, argc: cint, argv: ptr
 proc JS_DefineProperty*(ctx: JSContext, this_obj: JSValue, prop: JSAtom, val: JSValue, getter: JSValue, setter: JSValue, flags: cint): cint
 proc JS_DefinePropertyValue*(ctx: JSContext, this_obj: JSValue, prop: JSAtom, val: JSValue, flags: cint): cint
 proc JS_DefinePropertyValueUint32*(ctx: JSContext, this_obj: JSValue, idx: uint32, val: JSValue, flags: cint): cint
+proc JS_DefinePropertyValueInt64*(ctx: JSContext, this_obj: JSValue, idx: int64, val: JSValue, flags: cint): cint
 proc JS_DefinePropertyValueStr*(ctx: JSContext, this_obj: JSValue, prop: cstring, val: JSValue, flags: cint): cint
 proc JS_DefinePropertyValueGetSet*(ctx: JSContext, this_obj: JSValue, prop: JSAtom, getter: JSValue, setter: JSValue, flags: cint): cint
 
diff --git a/src/config/config.nim b/src/config/config.nim
index ddbbaa95..fe24db51 100644
--- a/src/config/config.nim
+++ b/src/config/config.nim
@@ -44,6 +44,7 @@ type
     nmap*: Table[string, string]
     lemap*: ActionMap
     stylesheet*: string
+    startup*: string
     ambiguous_double*: bool
     markcolor*: CellColor
 
diff --git a/src/css/match.nim b/src/css/match.nim
index a50f8396..7d502df1 100644
--- a/src/css/match.nim
+++ b/src/css/match.nim
@@ -203,15 +203,17 @@ func selectorsMatch*[T: Element|StyledNode](elem: T, selectors: ComplexSelector,
       return false
   return true
 
-proc querySelectorAll*(document: Document, q: string): seq[Element] =
+proc querySelectorAll(node: Node, q: string): seq[Element] =
   let selectors = parseSelectors(newStringStream(q))
-  for element in document.elements:
+  for element in node.elements:
     if element.selectorsMatch(selectors):
       result.add(element)
+doqsa = (proc(node: Node, q: string): seq[Element] = querySelectorAll(node, q))
 
-proc querySelector*(document: Document, q: string): Element =
+proc querySelector(node: Node, q: string): Element =
   let selectors = parseSelectors(newStringStream(q))
-  for element in document.elements:
+  for element in node.elements:
     if element.selectorsMatch(selectors):
       return element
   return nil
+doqs = (proc(node: Node, q: string): Element = querySelector(node, q))
diff --git a/src/display/client.nim b/src/display/client.nim
index 6e3c8c45..d06ec87c 100644
--- a/src/display/client.nim
+++ b/src/display/client.nim
@@ -1,11 +1,15 @@
 import options
 import os
 import streams
+import strutils
 import terminal
+import times
 import unicode
 
 import css/sheet
 import config/config
+import html/dom
+import html/htmlparser
 import io/buffer
 import io/cell
 import io/lineedit
@@ -149,12 +153,143 @@ proc readPipe(client: Client, ctype: string) =
   else:
     client.buffer.drawBuffer()
 
+type Cookie = ref object of RootObj
+  name {.jsget.}: string
+  value {.jsget.}: string
+  expires {.jsget.}: int64 # unix time
+  maxAge {.jsget.}: int64
+  secure {.jsget.}: bool
+  httponly {.jsget.}: bool
+  samesite {.jsget.}: bool
+  domain {.jsget.}: string
+  path {.jsget.}: string
+
+proc parseCookieDate(val: string): Option[DateTime] =
+  # cookie-date
+  const Delimiters = {'\t', ' '..'/', ';'..'@', '['..'`', '{'..'~'}
+  const NonDigit = Ascii + NonAscii - Digits
+  var foundTime = false
+  var foundDayOfMonth = false
+  var foundMonth = false
+  var foundYear = false
+  # date-token-list
+  var time: array[3, int]
+  var dayOfMonth: int
+  var month: int
+  var year: int
+  for dateToken in val.split(Delimiters):
+    if dateToken == "": continue # *delimiter
+    if not foundTime:
+      block timeBlock: # test for time
+        let hmsTime = dateToken.until(NonDigit - {':'})
+        var i = 0
+        for timeField in hmsTime.split(':'):
+          if i > 2: break timeBlock # too many time fields
+          # 1*2DIGIT
+          if timeField.len != 1 and timeField.len != 2: break timeBlock
+          var timeFields: array[3, int]
+          for c in timeField:
+            if c notin Digits: break timeBlock
+            timeFields[i] *= 10
+            timeFields[i] += c.decValue
+          time = timeFields
+          inc i
+        if i != 3: break timeBlock
+        foundTime = true
+        continue
+    if not foundDayOfMonth:
+      block dayOfMonthBlock: # test for day-of-month
+        let digits = dateToken.until(NonDigit)
+        if digits.len != 1 and digits.len != 2: break dayOfMonthBlock
+        var n = 0
+        for c in digits:
+          if c notin Digits: break dayOfMonthBlock
+          n *= 10
+          n += c.decValue
+        dayOfMonth = n
+        foundDayOfMonth = true
+        continue
+    if not foundMonth:
+      block monthBlock: # test for month
+        if dateToken.len < 3: break monthBlock
+        case dateToken.substr(0, 2).toLower()
+        of "jan": month = 1
+        of "feb": month = 2
+        of "mar": month = 3
+        of "apr": month = 4
+        of "may": month = 5
+        of "jun": month = 6
+        of "jul": month = 7
+        of "aug": month = 8
+        of "sep": month = 9
+        of "oct": month = 10
+        of "nov": month = 11
+        of "dec": month = 12
+        else: break monthBlock
+        foundMonth = true
+        continue
+    if not foundYear:
+      block yearBlock: # test for year
+        let digits = dateToken.until(NonDigit)
+        if digits.len != 2 and digits.len != 4: break yearBlock
+        var n = 0
+        for c in digits:
+          if c notin Digits: break yearBlock
+          n *= 10
+          n += c.decValue
+        year = n
+        foundYear = true
+        continue
+  if not (foundDayOfMonth and foundMonth and foundYear and foundTime): return none(DateTime)
+  if dayOfMonth notin 0..31: return none(DateTime)
+  if year < 1601: return none(DateTime)
+  if time[0] > 23: return none(DateTime)
+  if time[1] > 59: return none(DateTime)
+  if time[2] > 59: return none(DateTime)
+  var dateTime = dateTime(year, Month(month), MonthdayRange(dayOfMonth), HourRange(time[0]), MinuteRange(time[1]), SecondRange(time[2]))
+  return some(dateTime)
+
+proc parseCookie(client: Client, str: string): Cookie {.jsfunc.} =
+  let cookie = new(Cookie)
+  var first = true
+  for part in str.split(';'):
+    if first:
+      cookie.name = part.until('=')
+      cookie.value = part.after('=')
+      first = false
+      continue
+    let part = percentDecode(part).strip(leading = true, trailing = false, AsciiWhitespace)
+    var n = 0
+    for i in 0..part.high:
+      if part[i] == '=':
+        n = i
+        break
+    if n == 0:
+      continue
+    let key = part.substr(0, n - 1)
+    let val = part.substr(n + 1)
+    case key.toLower()
+    of "expires":
+      let date = parseCookieDate(val)
+      if date.issome:
+        cookie.expires = date.get.toTime().toUnix()
+    of "max-age": cookie.maxAge = parseInt64(val)
+    of "secure": cookie.secure = true
+    of "httponly": cookie.httponly = true
+    of "samesite": cookie.samesite = true
+    of "path": cookie.path = val
+    of "domain": cookie.domain = val
+  return cookie
+
+proc doRequest(client: Client, req: Request): Response {.jsfunc.} =
+  client.loader.doRequest(req)
+
 # Load request in a new buffer.
 var g_client: Client
 proc gotoUrl(client: Client, request: Request, prevurl = none(URL), force = false, ctype = "") =
   if force or prevurl.isnone or not prevurl.get.equals(request.url, true) or
       prevurl.get.equals(request.url) or request.httpmethod != HTTP_GET:
-    let page = client.loader.doRequest(request)
+    let page = client.doRequest(request)
     client.needsauth = page.status == 401 # Unauthorized
     client.redirecturl = page.redirect
     if page.body != nil:
@@ -262,10 +397,8 @@ proc evalJS(client: Client, src, filename: string): JSObject =
   unblockStdin()
   return client.jsctx.eval(src, filename, JS_EVAL_TYPE_GLOBAL)
 
-proc command(client: Client, src: string) =
-  restoreStdin()
-  let previ = client.console.err.getPosition()
-  let ret = client.evalJS(src, "<command>")
+proc command0(client: Client, src: string, filename = "<command>") =
+  let ret = client.evalJS(src, filename)
   if ret.isException():
     let ex = client.jsctx.getException()
     let str = ex.toString()
@@ -283,6 +416,11 @@ proc command(client: Client, src: string) =
     if str.issome:
       client.console.err.write(str.get & '\n')
   free(ret)
+
+proc command(client: Client, src: string) =
+  restoreStdin()
+  let previ = client.console.err.getPosition()
+  client.command0(src)
   g_client = client
   client.console.err.setPosition(previ)
   if client.console.lastbuf == nil or client.console.lastbuf != client.buffer:
@@ -418,8 +556,8 @@ proc isearchBack(client: Client) {.jsfunc.} =
     client.buffer.cpos = cpos
 
 proc quit(client: Client) {.jsfunc.} =
-  eraseScreen()
-  print(HVP(0, 0))
+  #eraseScreen()
+  #print(HVP(0, 0))
   quit(0)
 
 proc feedNext(client: Client) {.jsfunc.} =
@@ -527,7 +665,25 @@ proc inputLoop(client: Client) =
     except ActionError as e:
       client.buffer.setStatusMessage(e.msg)
 
+#TODO this is dumb
+proc readFile(client: Client, path: string): string {.jsfunc.} =
+  try:
+    return readFile(path)
+  except IOError:
+    discard
+
+#TODO ditto
+proc writeFile(client: Client, path: string, content: string) {.jsfunc.} =
+  writeFile(path, content)
+
 proc launchClient*(client: Client, pages: seq[string], ctype: string, dump: bool) =
+  if gconfig.startup != "":
+    let s = readFile(gconfig.startup)
+    #client.command(s)
+    client.console.err = newFileStream(stderr)
+    client.command0(s, gconfig.startup)
+    client.console.err = newStringStream()
+    quit()
   client.userstyle = gconfig.stylesheet.parseStylesheet()
   if not stdin.isatty:
     client.readPipe(ctype)
@@ -569,6 +725,9 @@ proc log(console: Console, ss: varargs[string]) {.jsfunc.} =
       console.err.write(' ')
   console.err.write('\n')
 
+proc sleep(client: Client, millis: int) {.jsfunc.} =
+  sleep millis
+
 proc newClient*(): Client =
   new(result)
   result.loader = newFileLoader()
@@ -579,13 +738,18 @@ proc newClient*(): Client =
   result.jsrt = rt
   result.jsctx = ctx
   var global = ctx.getGlobalObject()
-  discard ctx.registerType(Client, asglobal = true, addto = some(global))
+  ctx.registerType(Cookie)
+  ctx.registerType(Client, asglobal = true)
   global.setOpaque(result)
-  global.setProperty("client", global)
+  ctx.setProperty(global.val, "client", global.val)
 
   let consoleClassId = ctx.registerType(Console)
   let jsConsole = ctx.newJSObject(consoleClassId)
   jsConsole.setOpaque(result.console)
-  global.setProperty("console", jsConsole)
+  ctx.setProperty(global.val, "console", jsConsole.val)
+  free(global)
 
   ctx.addUrlModule()
+  ctx.addDOMModule()
+  ctx.addHTMLModule()
+  ctx.addRequestModule()
diff --git a/src/html/dom.nim b/src/html/dom.nim
index 4bf73302..9c0f0a59 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -5,6 +5,7 @@ import tables
 
 import css/sheet
 import html/tags
+import js/javascript
 import types/url
 import utils/twtstr
 
@@ -34,7 +35,7 @@ type
 
   Node* = ref object of EventTarget
     nodeType*: NodeType
-    childNodes*: seq[Node]
+    childNodes* {.jsget.}: seq[Node]
     nextSibling*: Node
     previousSibling*: Node
     parentNode*: Node
@@ -83,7 +84,7 @@ type
 
     id*: string
     classList*: seq[string]
-    attributes*: Table[string, string]
+    attributes* {.jsget, jsset.}: Table[string, string]
     hover*: bool
     invalid*: bool
 
@@ -97,7 +98,7 @@ type
     inputType*: InputType
     autofocus*: bool
     required*: bool
-    value*: string
+    value* {.jsget.}: string
     size*: int
     checked*: bool
     xcoord*: int
@@ -129,7 +130,7 @@ type
     start*: Option[int]
 
   HTMLLIElement* = ref object of HTMLElement
-    value*: Option[int]
+    value* {.jsget.}: Option[int]
 
   HTMLStyleElement* = ref object of HTMLElement
     sheet*: CSSStylesheet
@@ -199,6 +200,11 @@ iterator children_rev*(node: Node): Element {.inline.} =
     if child.nodeType == ELEMENT_NODE:
       yield Element(child)
 
+#TODO TODO TODO this should return a live view instead
+proc children*(node: Node): seq[Element] {.jsget.} =
+  for child in node.children:
+    result.add(child)
+
 # Returns the node's ancestors
 iterator ancestors*(node: Node): Element {.inline.} =
   var element = node.parentElement
@@ -415,7 +421,7 @@ func attrb*(element: Element, s: string): bool =
     return true
   return false
 
-func textContent*(node: Node): string =
+func textContent*(node: Node): string {.jsget.} =
   case node.nodeType
   of DOCUMENT_NODE, DOCUMENT_TYPE_NODE:
     return "" #TODO null
@@ -533,7 +539,7 @@ func formmethod*(element: Element): FormMethod =
 
   return FORM_METHOD_GET
 
-func target*(element: Element): string =
+func target*(element: Element): string {.jsfunc.} =
   if element.attrb("target"):
     return element.attr("target")
   for base in element.document.elements(TAG_BASE):
@@ -547,13 +553,13 @@ func findAncestor*(node: Node, tagTypes: set[TagType]): Element =
       return element
   return nil
 
-func newText*(document: Document, data: string = ""): Text =
+func newText*(document: Document, data: string = ""): Text {.jsctor.} =
   new(result)
   result.nodeType = TEXT_NODE
   result.document = document
   result.data = data
 
-func newComment*(document: Document, data: string = ""): Comment =
+func newComment*(document: Document = nil, data: string = ""): Comment {.jsctor.} =
   new(result)
   result.nodeType = COMMENT_NODE
   result.document = document
@@ -617,12 +623,12 @@ func newHTMLElement*(document: Document, localName: string, namespace = Namespac
   if tagType == TAG_UNKNOWN:
     result.localName = localName
 
-func newDocument*(): Document =
+func newDocument*(): Document {.jsctor.} =
   new(result)
   result.nodeType = DOCUMENT_NODE
   result.document = result
 
-func newDocumentType*(document: Document, name: string, publicId = "", systemId = ""): DocumentType =
+func newDocumentType*(document: Document, name: string, publicId = "", systemId = ""): DocumentType {.jsctor.} =
   new(result)
   result.document = document
   result.name = name
@@ -637,13 +643,20 @@ func newAttr*(parent: Element, key, value: string): Attr =
   result.name = key
   result.value = value
 
-func getElementById*(node: Node, id: string): Element =
+func getElementById*(node: Node, id: string): Element {.jsfunc.} =
   if id.len == 0:
     return nil
   for child in node.elements:
     if child.id == id:
       return child
 
+func getElementById2*(node: Node, id: string): pointer =
+  if id.len == 0:
+    return nil
+  for child in node.elements:
+    if child.id == id:
+      return cast[pointer](child)
+
 func getElementsByTag*(document: Document, tag: TagType): seq[Element] =
   for element in document.elements(tag):
     result.add(element)
@@ -709,7 +722,7 @@ func text*(option: HTMLOptionElement): string =
       if child.parentElement.tagType != TAG_SCRIPT: #TODO svg
         result &= child.data.stripAndCollapse()
 
-func value*(option: HTMLOptionElement): string =
+func value*(option: HTMLOptionElement): string {.jsget.} =
   if option.attrb("value"):
     return option.attr("value")
   return option.childTextContent.stripAndCollapse()
@@ -960,3 +973,41 @@ proc appendAttribute*(element: Element, k, v: string) =
         select.size = 4
   else: discard
   element.attributes[k] = v
+
+var doqsa*: proc (node: Node, q: string): seq[Element]
+var doqs*: proc (node: Node, q: string): Element
+
+proc querySelectorAll*(node: Node, q: string): seq[Element] {.jsfunc.} =
+  return doqsa(node, q)
+
+proc querySelector*(node: Node, q: string): Element {.jsfunc.} =
+  return doqs(node, q)
+
+proc addDOMModule*(ctx: JSContext) =
+  let eventTargetCID = ctx.registerType(EventTarget)
+  let nodeCID = ctx.registerType(Node, parent = eventTargetCID)
+  ctx.registerType(Document, parent = nodeCID)
+  let characterDataCID = ctx.registerType(CharacterData, parent = nodeCID)
+  ctx.registerType(Comment, parent = characterDataCID)
+  ctx.registerType(Text, parent = characterDataCID)
+  ctx.registerType(DocumentType, parent = nodeCID)
+  let elementCID = ctx.registerType(Element, parent = nodeCID)
+  let htmlElementCID = ctx.registerType(HTMLElement, parent = elementCID)
+  ctx.registerType(HTMLInputElement, parent = htmlElementCID)
+  ctx.registerType(HTMLAnchorElement, parent = htmlElementCID)
+  ctx.registerType(HTMLSelectElement, parent = htmlElementCID)
+  ctx.registerType(HTMLSpanElement, parent = htmlElementCID)
+  ctx.registerType(HTMLOptGroupElement, parent = htmlElementCID)
+  ctx.registerType(HTMLOptionElement, parent = htmlElementCID)
+  ctx.registerType(HTMLHeadingElement, parent = htmlElementCID)
+  ctx.registerType(HTMLBRElement, parent = htmlElementCID)
+  ctx.registerType(HTMLMenuElement, parent = htmlElementCID)
+  ctx.registerType(HTMLUListElement, parent = htmlElementCID)
+  ctx.registerType(HTMLOListElement, parent = htmlElementCID)
+  ctx.registerType(HTMLLIElement, parent = htmlElementCID)
+  ctx.registerType(HTMLStyleElement, parent = htmlElementCID)
+  ctx.registerType(HTMLLinkElement, parent = htmlElementCID)
+  ctx.registerType(HTMLFormElement, parent = htmlElementCID)
+  ctx.registerType(HTMLTemplateElement, parent = htmlElementCID)
+  ctx.registerType(HTMLUnknownElement, parent = htmlElementCID)
+  ctx.registerType(HTMLScriptElement, parent = htmlElementCID)
diff --git a/src/html/htmlparser.nim b/src/html/htmlparser.nim
index a8ea5458..f28cd300 100644
--- a/src/html/htmlparser.nim
+++ b/src/html/htmlparser.nim
@@ -6,13 +6,16 @@ import strformat
 import tables
 import unicode
 
-import utils/twtstr
+import css/sheet
 import html/dom
 import html/tags
 import html/htmltokenizer
-import css/sheet
+import js/javascript
+import utils/twtstr
 
 type
+  DOMParser = ref object # JS interface
+
   HTML5Parser = object
     case fragment: bool
     of true: ctx: Element
@@ -2085,3 +2088,18 @@ proc parseHTML5*(inputStream: Stream): Document =
   parser.document = newDocument()
   parser.tokenizer = inputStream.newTokenizer()
   return parser.constructTree()
+
+proc newDOMParser*(): DOMParser {.jsctor.} =
+  new(result)
+
+proc parseFromString*(parser: DOMParser, str: string, t: string): Document {.jserr, jsfunc.} =
+  case t
+  of "text/html":
+    return parseHTML5(newStringStream(str))
+  of "text/xml", "application/xml", "application/xhtml+xml", "image/svg+xml":
+    JS_THROW JS_InternalError, "XML parsing is not supported yet"
+  else:
+    JS_THROW JS_TypeError, "Invalid mime type"
+
+proc addHTMLModule*(ctx: JSContext) =
+  ctx.registerType(DOMParser)
diff --git a/src/html/htmltokenizer.nim b/src/html/htmltokenizer.nim
index a1c894c8..c8f96144 100644
--- a/src/html/htmltokenizer.nim
+++ b/src/html/htmltokenizer.nim
@@ -227,7 +227,7 @@ iterator tokenize*(tokenizer: var Tokenizer): Token =
   template emit_tmp() =
     var i = 0
     while i < tokenizer.tmp.len:
-      if tokenizer.tmp[i].isAscii():
+      if tokenizer.tmp[i] in Ascii:
         emit tokenizer.tmp[i]
         inc i
       else:
diff --git a/src/io/lineedit.nim b/src/io/lineedit.nim
index def2feb2..3f9034bf 100644
--- a/src/io/lineedit.nim
+++ b/src/io/lineedit.nim
@@ -167,8 +167,8 @@ proc readLine(state: var LineState): bool =
         else:
           state.fullRedraw()
     of ACTION_LINED_DELETE:
-      if state.cursor > 0 and state.cursor < state.news.len:
-        let w = state.news[state.cursor - 1].lwidth()
+      if state.cursor >= 0 and state.cursor < state.news.len:
+        let w = state.news[state.cursor].lwidth()
         state.news.delete(state.cursor..state.cursor)
         if state.cursor == state.news.len and state.shift == 0:
           state.kill(w)
diff --git a/src/io/loader.nim b/src/io/loader.nim
index 7c68258e..1530bf5b 100644
--- a/src/io/loader.nim
+++ b/src/io/loader.nim
@@ -100,6 +100,7 @@ proc runFileLoader(loader: FileLoader, loadcb: proc()) =
   quit(0)
 
 proc doRequest*(loader: FileLoader, request: Request): Response =
+  new(result)
   let stream = connectSocketStream(loader.process)
   stream.swrite(request)
   stream.flush()
diff --git a/src/io/request.nim b/src/io/request.nim
index 7553bdb5..6e6f3f40 100644
--- a/src/io/request.nim
+++ b/src/io/request.nim
@@ -2,12 +2,20 @@ import options
 import streams
 import tables
 
-import utils/twtstr
 import types/url
+import js/javascript
+import utils/twtstr
 
 type HttpMethod* = enum
-  HTTP_CONNECT, HTTP_DELETE, HTTP_GET, HTTP_HEAD, HTTP_OPTIONS, HTTP_PATCH,
-  HTTP_POST, HTTP_PUT, HTTP_TRACE
+  HTTP_CONNECT = "CONNECT"
+  HTTP_DELETE = "DELETE"
+  HTTP_GET = "GET"
+  HTTP_HEAD = "HEAD"
+  HTTP_OPTIONS = "OPTIONS"
+  HTTP_PATCH = "PATCH"
+  HTTP_POST = "POST"
+  HTTP_PUT = "PUT"
+  HTTP_TRACE = "TRACE"
 
 type
   Request* = ref RequestObj
@@ -18,13 +26,13 @@ type
     body*: Option[string]
     multipart*: Option[MimeData]
 
-  Response* = object
+  Response* = ref object
     body*: Stream
-    res*: int
-    contenttype*: string
-    status*: int
-    headers*: HeaderList
-    redirect*: Option[Url]
+    res* {.jsget.}: int
+    contenttype* {.jsget.}: string
+    status* {.jsget.}: int
+    headers* {.jsget.}: HeaderList
+    redirect* {.jsget.}: Option[Url]
 
   ReadableStream* = ref object of Stream
     isource*: Stream
@@ -32,7 +40,7 @@ type
     isend: bool
 
   HeaderList* = ref object
-    table*: Table[string, seq[string]]
+    table* {.jsget.}: Table[string, seq[string]]
 
 # Originally from the stdlib
   MimePart* = object
@@ -122,9 +130,9 @@ func newHeaderList*(table: Table[string, string]): HeaderList =
 
 func newRequest*(url: Url,
                  httpmethod = HTTP_GET,
-                 headers: openarray[(string, string)] = [],
+                 headers: seq[(string, string)] = @[],
                  body = none(string),
-                 multipart = none(MimeData)): Request =
+                 multipart = none(MimeData)): Request {.jsctor.} =
   new(result)
   result.httpmethod = httpmethod
   result.url = url
@@ -135,6 +143,16 @@ func newRequest*(url: Url,
   result.body = body
   result.multipart = multipart
 
+func newRequest*(url: Url,
+                 httpmethod: HttpMethod,
+                 headers: openarray[(string, string)],
+                 body = none(string),
+                 multipart = none(MimeData)): Request =
+  var s: seq[(string, string)]
+  for it in headers:
+    s.add(it)
+  return newRequest(url, httpmethod, s, body, multipart)
+
 proc `[]=`*(multipart: var MimeData, k, v: string) =
   multipart.content.add(MimePart(name: k, content: v))
 
@@ -154,3 +172,11 @@ func getOrDefault*(headers: HeaderList, k: string, default = ""): string =
     headers.table[k][0]
   else:
     default
+
+proc readAll*(response: Response): string {.jsfunc.} =
+  return response.body.readAll()
+
+proc addRequestModule*(ctx: JSContext) =
+  ctx.registerType(Request)
+  ctx.registerType(Response)
+  ctx.registerType(HeaderList)
diff --git a/src/js/javascript.nim b/src/js/javascript.nim
index 11253875..9ba4e87f 100644
--- a/src/js/javascript.nim
+++ b/src/js/javascript.nim
@@ -30,9 +30,12 @@ type
 
   JSContextOpaque* = ref object
     creg: Table[string, JSClassID]
+    typemap: Table[pointer, JSClassID]
+    ctors: Table[JSClassID, JSValue] #TODO TODO TODO free these
     gclaz: string
     sym_iterator: JSAtom
     sym_asyncIterator: JSAtom
+    sym_toStringTag: JSAtom
     done: JSAtom
     next: JSAtom
     value: JSAtom
@@ -78,6 +81,11 @@ proc newJSContext*(rt: JSRuntime): JSContext =
         opaque.sym_asyncIterator = JS_ValueToAtom(ctx, ait)
         JS_FreeValue(ctx, ait)
       block:
+        let ait = JS_GetPropertyStr(ctx, sym, "toStringTag")
+        assert JS_IsSymbol(ait)
+        opaque.sym_toStringTag = JS_ValueToAtom(ctx, ait)
+        JS_FreeValue(ctx, ait)
+      block:
         let s = "done"
         opaque.done = JS_NewAtomLen(ctx, cstring(s), csize_t(s.len))
       block:
@@ -131,9 +139,8 @@ func newJSObject*(ctx: JSContext, class: string): JSObject =
   result.ctx = ctx
   result.val = JS_NewObjectClass(ctx, ctx.getClass(class))
 
-func newJSCFunction*(ctx: JSContext, name: string, fun: JSCFunction, argc: int = 0, proto = JS_CFUNC_generic, magic = 0): JSObject =
-  result.ctx = ctx
-  result.val = JS_NewCFunction2(ctx, fun, cstring(name), cint(argc), proto, cint(magic))
+func newJSCFunction*(ctx: JSContext, name: string, fun: JSCFunction, argc: int = 0, proto = JS_CFUNC_generic, magic = 0): JSValue =
+  return JS_NewCFunction2(ctx, fun, cstring(name), cint(argc), proto, cint(magic))
 
 func getGlobalObject*(ctx: JSContext): JSObject =
   result.ctx = ctx
@@ -152,6 +159,7 @@ proc free*(ctx: var JSContext) =
   if opaque != nil:
     JS_FreeAtom(ctx, opaque.sym_iterator)
     JS_FreeAtom(ctx, opaque.sym_asyncIterator)
+    JS_FreeAtom(ctx, opaque.sym_toStringTag)
     JS_FreeAtom(ctx, opaque.done)
     JS_FreeAtom(ctx, opaque.next)
     GC_unref(opaque)
@@ -183,7 +191,6 @@ func isGlobal*(ctx: JSContext, class: string): bool =
   return ctx.getOpaque().gclaz == class
 
 # A hack to retrieve a given val's class id.
-# Used when we can't query ctx's hash table.
 func getClassID*(val: JSValue): JSClassID =
   const index = sizeof(cint) + # gc_ref_count
               sizeof(uint8) + # gc_mark
@@ -199,7 +206,7 @@ func getOpaque*(ctx: JSContext, val: JSValue, class: string): pointer =
     let opaque = JS_GetOpaque(global.val, 1) # JS_CLASS_OBJECT
     free(global)
     return opaque
-  return JS_GetOpaque(val, ctx.getClass(class))
+  return JS_GetOpaque(val, val.getClassID())
 
 func getOpaque*(obj: JSObject, class: string): pointer = getOpaque(obj.ctx, obj.val, class)
 
@@ -230,46 +237,59 @@ func isGlobal*(obj: JSObject): bool =
   let global = obj.ctx.getGlobalObject()
   result = JS_VALUE_GET_PTR(global.val) == JS_VALUE_GET_PTR(obj.val)
 
-func isInstanceOf*(obj: JSObject, class: string): bool =
-  return obj.getOpaque(class) != nil
+func isInstanceOf*(ctx: JSContext, obj: JSValue, class: string): bool =
+  let clazz = ctx.getClass(class)
+  if clazz in ctx.getOpaque().ctors:
+    let ctor = ctx.getOpaque().ctors[clazz]
+    if JS_IsInstanceOf(ctx, obj, ctor) == 1:
+      return true
+    return false #TODO handle exception?
+  else:
+    #TODO TODO TODO LegacyNoInterfaceObject has no constructor...
+    return false
 
-proc setProperty*(obj: JSObject, name: string, prop: JSObject) =
-  if JS_SetPropertyStr(obj.ctx, obj.val, cstring(name), prop.val) <= 0:
+proc setProperty*(ctx: JSContext, val: JSValue, name: string, prop: JSValue) =
+  if JS_SetPropertyStr(ctx, val, cstring(name), prop) <= 0:
     raise newException(Defect, "Failed to set property string: " & name)
 
-proc setProperty*(obj: JSObject, name: string, fun: JSCFunction, argc: int = 0) =
-  obj.setProperty(name, obj.ctx.newJSCFunction(name, fun, argc))
+proc setProperty*(ctx: JSContext, val: JSValue, name: string, fun: JSCFunction, argc: int = 0) =
+  ctx.setProperty(val, name, ctx.newJSCFunction(name, fun, argc))
 
-func newJSClass*(ctx: JSContext, cdef: JSClassDefConst, cctor: JSCFunction, funcs: JSFunctionList, parent: JSClassID = 0, asglobal = false, addto = none(JSObject)): JSClassID =
+func newJSClass*(ctx: JSContext, cdef: JSClassDefConst, cctor: JSCFunction, funcs: JSFunctionList, nimt: pointer, parent: JSClassID, asglobal: bool, nointerface: bool): JSClassID {.discardable.} =
   let rt = JS_GetRuntime(ctx)
   discard JS_NewClassID(addr result)
   var ctxOpaque = ctx.getOpaque()
   var rtOpaque = rt.getOpaque()
-  ctxOpaque.creg[$cdef.class_name] = result
   if JS_NewClass(rt, result, cdef) != 0:
     raise newException(Defect, "Failed to allocate JS class: " & $cdef.class_name)
-  var proto: JSObject
+  ctxOpaque.typemap[nimt] = result
+  ctxOpaque.creg[$cdef.class_name] = result
+  var proto: JSValue
   if parent != 0:
-    let parentProto = ctx.getClassProto(parent)
-    proto = ctx.newJSObject(parentProto)
-    free(parentProto)
+    let parentProto = JS_GetClassProto(ctx, parent)
+    proto = JS_NewObjectProtoClass(ctx, parentProto, parent)
+    JS_FreeValue(ctx, parentProto)
   else:
-    proto = ctx.newJSObject()
+    proto = JS_NewObject(ctx)
   if funcs.len > 0:
     rtOpaque.flist.add(funcs.toSeq())
-    JS_SetPropertyFunctionList(ctx, proto.val, addr rtOpaque.flist[^1][0], cint(funcs.len))
-  JS_SetClassProto(ctx, result, proto.val)
+    JS_SetPropertyFunctionList(ctx, proto, addr rtOpaque.flist[^1][0], cint(funcs.len))
+  assert JS_SetProperty(ctx, proto, ctxOpaque.sym_toStringTag, JS_NewString(ctx, cdef.class_name)) == 1
+  JS_SetClassProto(ctx, result, proto)
   if asglobal:
     let global = ctx.getGlobalObject()
     assert ctxOpaque.gclaz == ""
     ctxOpaque.gclaz = $cdef.class_name
-    if JS_SetPrototype(ctx, global.val, proto.val) != 1:
+    if JS_SetPrototype(ctx, global.val, proto) != 1:
       raise newException(Defect, "Failed to set global prototype: " & $cdef.class_name)
     free(global)
-  if addto.issome:
+  if not nointerface:
+    let global = JS_GetGlobalObject(ctx)
     let jctor = ctx.newJSCFunction($cdef.class_name, cctor, 0, JS_CFUNC_constructor)
-    JS_SetConstructor(ctx, jctor.val, proto.val)
-    addto.get.setProperty($cdef.class_name, jctor)
+    JS_SetConstructor(ctx, jctor, proto)
+    ctxOpaque.ctors[result] = JS_DupValue(ctx, jctor)
+    ctx.setProperty(global, $cdef.class_name, jctor)
+    JS_FreeValue(ctx, global)
 
 type FuncParam = tuple[name: string, t: NimNode, val: Option[NimNode], generic: Option[NimNode]]
 
@@ -291,12 +311,12 @@ func fromJSInt[T: SomeInteger](ctx: JSContext, val: JSValue): Option[T] =
       var ret: int32
       if JS_ToInt32(ctx, addr ret, val) < 0:
         return none(T)
-      return some(ret)
+      return some(int(ret))
     else:
       var ret: int64
       if JS_ToInt64(ctx, addr ret, val) < 0:
         return none(T)
-      return some(ret)
+      return some(int(ret))
   elif T is uint:
     when sizeof(int) <= sizeof(int32):
       var ret: uint32
@@ -367,6 +387,14 @@ macro fromJSTupleBody(a: tuple) =
     )
     if i == len - 1:
       result.add(quote do:
+        let next = JS_Call(ctx, next_method, it, 0, nil)
+        if JS_IsException(next):
+          return none(T)
+        defer: JS_FreeValue(ctx, next)
+        let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().done)
+        `done` = fromJS[bool](ctx, doneVal)
+        if `done`.isnone: # exception
+          return none(T)
         var i = `i`
         # we're simulating a sequence, so we must query all remaining parameters too:
         while not `done`.get:
@@ -402,8 +430,8 @@ proc fromJSTuple[T: tuple](ctx: JSContext, val: JSValue): Option[T] =
     return none(T)
   defer: JS_FreeValue(ctx, next_method)
   var x: T
-  result = some(x)
-  fromJSTupleBody(result.get)
+  fromJSTupleBody(x)
+  return some(x)
 
 proc fromJSSeq[T](ctx: JSContext, val: JSValue): Option[seq[T]] =
   let itprop = JS_GetProperty(ctx, val, ctx.getOpaque().sym_iterator)
@@ -444,7 +472,6 @@ proc fromJSSeq[T](ctx: JSContext, val: JSValue): Option[seq[T]] =
 
 proc fromJSTable[A, B](ctx: JSContext, val: JSValue): Option[Table[A, B]] =
   var ptab: ptr JSPropertyEnum
-  #TODO free ptab
   var plen: uint32
   let flags = cint(JS_GPN_STRING_MASK)
   if JS_GetOwnPropertyNames(ctx, addr ptab, addr plen, val, flags) < -1:
@@ -471,12 +498,14 @@ proc fromJSTable[A, B](ctx: JSContext, val: JSValue): Option[Table[A, B]] =
       return none(Table[A, B])
     result.get[kn.get] = vn.get
 
-#TODO handle exceptions in all cases
 proc fromJS[T](ctx: JSContext, val: JSValue): Option[T] =
   when T is string:
     return toString(ctx, val)
-  elif typeof(result.get) is Option: # unwrap
-    return fromJS[typeof(result.get)](ctx, val)
+  elif typeof(result.unsafeGet) is Option: # unwrap
+    let res = fromJS[typeof(result.get.get)](ctx, val)
+    if res.isnone:
+      return none(T)
+    return some(res)
   elif T is seq:
     return fromJSSeq[typeof(result.get[0])](ctx, val)
   elif T is tuple:
@@ -491,13 +520,30 @@ proc fromJS[T](ctx: JSContext, val: JSValue): Option[T] =
   elif typeof(result.get) is Table:
     return fromJSTable[typeof(result.get.keys), typeof(result.get.values)](ctx, val)
   elif T is SomeInteger:
-    return fromJSInt[T](obj.ctx, obj.val)
+    return fromJSInt[T](ctx, val)
   elif T is SomeFloat:
     let f64: float64
     if JS_ToFloat64(ctx, addr f64, val) < 0:
       return none(T)
     return some(cast[T](f64))
+  elif T is enum:
+    #TODO implement enum handling...
+    if JS_IsException(val):
+      return none(T)
+    let s = toString(ctx, val)
+    if s.isnone:
+      return none(T)
+    try:
+      return some(parseEnum[T](s.get))
+    except ValueError:
+      JS_ThrowTypeError(ctx, "`%s' is not a valid value for enumeration %s", cstring(s.get), $T)
+      return none(T)
+  elif T is object:
+    #TODO TODO TODO dictionary case
+    return none(T)
   else:
+    if JS_IsException(val):
+      return none(T)
     let op = cast[T](getOpaque(ctx, val, $T))
     if op == nil:
       JS_ThrowTypeError(ctx, "Value is not an instance of %s", $T)
@@ -510,7 +556,7 @@ func toJSString(ctx: JSContext, str: string): JSValue =
 func toJSInt(ctx: JSContext, n: SomeInteger): JSValue =
   when n is int:
     when sizeof(int) <= sizeof(int32):
-      return JS_NewInt32(ctx, n)
+      return JS_NewInt32(ctx, int32(n))
     else:
       return JS_NewInt64(ctx, n)
   elif n is uint:
@@ -529,34 +575,62 @@ func toJSInt(ctx: JSContext, n: SomeInteger): JSValue =
 
 func toJSNumber(ctx: JSContext, n: SomeNumber): JSValue =
   when n is SomeInteger:
-    return toJSInt(n)
+    return toJSInt(ctx, n)
   else:
-    return JS_NewFloat64(n)
+    return JS_NewFloat64(ctx, n)
 
 func toJSBool(ctx: JSContext, b: bool): JSValue =
   return JS_NewBool(ctx, b)
 
-func toJSObject[T](ctx: JSContext, obj: T, class: string): JSValue =
-  let claz = ctx.findClass(class)
-  assert claz.issome
+proc getTypePtr[T](x: T): pointer =
+  when T is RootRef:
+    # I'm so sorry.
+    # (This dereferences the object's first member, m_type. Probably.)
+    return cast[ptr pointer](x)[]
+  else:
+    return getTypeInfo(x)
+
+func toJSObject[T](ctx: JSContext, obj: T): JSValue =
   let op = JS_GetRuntime(ctx).getOpaque()
   let p = cast[pointer](obj)
   if p in op.plist:
     # a JSValue already points to this object.
     return JS_DupValue(ctx, JS_MKPTR(JS_TAG_OBJECT, op.plist[p]))
-  let jsObj = ctx.newJSObject(claz.get)
+  let clazz = ctx.getOpaque().typemap[getTypePtr(obj)]
+  let jsObj = ctx.newJSObject(clazz)
   jsObj.setOpaque(obj)
   return jsObj.val
 
-proc toJS*[T](ctx: JSContext, obj: T, class = ""): JSValue =
+proc toJS*[T](ctx: JSContext, obj: T): JSValue =
   when T is string:
-    ctx.toJSString(obj)
+    return ctx.toJSString(obj)
   elif T is SomeNumber:
-    ctx.toJSNumber(obj)
+    return ctx.toJSNumber(obj)
   elif T is bool:
-    ctx.toJSBool(obj)
+    return ctx.toJSBool(obj)
+  elif T is Table:
+    result = JS_NewObject(ctx)
+    if not JS_IsException(result):
+      for k, v in obj:
+        setProperty(ctx, result, k, toJS(ctx, v))
+  elif T is Option:
+    if obj.issome:
+      return toJS(ctx, obj.get)
+    return JS_NULL
+  elif T is seq:
+    let a = JS_NewArray(ctx)
+    if not JS_IsException(a):
+      for i in 0..obj.high:
+        let j = toJS(ctx, obj[i])
+        if JS_IsException(j):
+          return j
+        if JS_DefinePropertyValueInt64(ctx, a, int64(i), j, JS_PROP_C_W_E or JS_PROP_THROW) < 0:
+          return JS_EXCEPTION
+    return a
   else:
-    ctx.toJSObject(obj, class)
+    if obj == nil:
+      return JS_NULL
+    return ctx.toJSObject(obj)
 
 type
   JS_Error = object of CatchableError
@@ -642,7 +716,8 @@ proc getParams(fun: NimNode): seq[FuncParam] =
       quote do:
         typeof(`x`)
     let val = if it[2].kind != nnkEmpty:
-      some(it[2])
+      let x = it[2]
+      some(newPar(x))
     else:
       none(NimNode)
     var g = none(NimNode)
@@ -738,16 +813,17 @@ proc addUnionParam(gen: var JSFuncGenerator, tt: NimNode, s: NimNode, fallback:
     elif g == string.getType():
       hasString = true
   # 4. If V is null or undefined, then:
-  if tableg.issome:
-    let a = tableg.get[1]
-    let b = tableg.get[2]
-    gen.addUnionParamBranch(quote do: (
-        let val = getJSValue(ctx, argv, `j`)
-        JS_IsNull(val) or JS_IsUndefined(val)
-      ),
-      quote do:
-        let `s` = Table[`a`, `b`](), #TODO is this correct?
-      fallback)
+  #TODO this is wrong. map dictionary to object instead
+  #if tableg.issome:
+  #  let a = tableg.get[1]
+  #  let b = tableg.get[2]
+  #  gen.addUnionParamBranch(quote do: (
+  #      let val = getJSValue(ctx, argv, `j`)
+  #      JS_IsNull(val) or JS_IsUndefined(val)
+  #    ),
+  #    quote do:
+  #      let `s` = Table[`a`, `b`](),
+  #    fallback)
   # 10. If Type(V) is Object, then:
   if tableg.issome or seqg.issome:
     # Sequence:
@@ -860,8 +936,7 @@ proc newJSProcBody(gen: var JSFuncGenerator, isva: bool): NimNode =
     )
   if gen.hasthis:
     result.add(quote do:
-      var this = JSObject(ctx: ctx, val: this)
-      if not (this.isUndefined() or ctx.isGlobal(`tt`)) and not this.isInstanceOf(`tt`):
+      if not (JS_IsUndefined(this) or ctx.isGlobal(`tt`)) and not isInstanceOf(ctx, this, `tt`):
         # undefined -> global.
         return JS_ThrowTypeError(ctx, "'%s' called on an object that is not an instance of %s", `fn`, `tt`)
     )
@@ -870,16 +945,13 @@ proc newJSProcBody(gen: var JSFuncGenerator, isva: bool): NimNode =
     var tryWrap = newNimNode(nnkTryStmt)
     tryWrap.add(gen.jsCallAndRet)
     for error in js_errors[gen.funcName]:
-      let ename = ident("JS_" & error)
+      let ename = ident(error)
       var exceptBranch = newNimNode(nnkExceptBranch)
       let eid = ident("e")
       exceptBranch.add(newNimNode(nnkInfix).add(ident("as"), ename, eid))
-      let throwName = ident("JS_Throw" & error)
-      exceptBranch.add(newCall(throwName,
-                               ident("ctx"),
-                               newLit("%s"),
-                               newCall(ident("cstring"),
-                                       newDotExpr(eid, ident("msg")))))
+      let throwName = ident("JS_Throw" & error.substr("JS_".len))
+      exceptBranch.add(quote do:
+        return `throwName`(ctx, "%s", cstring(`eid`.msg)))
       tryWrap.add(exceptBranch)
     gen.jsCallAndRet = tryWrap
   result.add(gen.jsCallAndRet)
@@ -890,14 +962,15 @@ proc newJSProc(gen: var JSFuncGenerator, params: openArray[NimNode], isva = true
   result = newProc(ident(gen.newName), params, jsBody, pragmas = jsPragmas)
   gen.res = result
 
+# WARNING: for now, this only works correctly when the .jserr pragma was
+# declared on the parent function.
 # Note: this causes the entire nim function body to be inlined inside the JS
 # interface function.
 #TODO: implement actual inlining (so we can e.g. get rid of JS_Error, use format strings, etc.)
-macro JS_THROW*(a: untyped, b: string) =
-  let es = ident("JS_" & a.strVal)
+macro JS_THROW*(a: typed, b: string) =
   result = quote do:
     block when_js:
-      raise newException(`es`, `b`)
+      raise newException(`a`, `b`)
 
 proc setupGenerator(fun: NimNode, hasthis = true, hasfuncall = true): JSFuncGenerator =
   result.funcName = $fun[0]
@@ -952,7 +1025,7 @@ macro jserr*(fun: untyped) =
   gen.rewriteExceptions()
   var pragma = gen.original.findChild(it.kind == nnkPragma)
   for i in 0..<pragma.len:
-    if pragma[i] == ident("jsctor"):
+    if pragma[i].eqIdent(ident("jsctor")) or pragma[i].eqIdent(ident("jsfunc")) or pragma[i].eqIdent(ident("jsget")) or pragma[i].eqIdent(ident("jsset")):
       pragma.del(i)
   gen.copied.addPragma(quote do: inline)
 
@@ -974,9 +1047,8 @@ macro jsctor*(fun: typed) =
     gen.addOptionalParams()
     gen.finishFunCallList()
     let jfcl = gen.jsFunCallList
-    let tt = gen.thisType
     gen.jsCallAndRet = quote do:
-      return ctx.toJS((`jfcl`), `tt`)
+      return ctx.toJS(`jfcl`)
     discard gen.newJSProc(getJSParams())
     gen.registerFunction()
     result = newStmtList(fun)
@@ -992,9 +1064,8 @@ macro jsget*(fun: typed) =
   gen.addFixParam("this")
   gen.finishFunCallList()
   let jfcl = gen.jsFunCallList
-  let tt = gen.thisType
   gen.jsCallAndRet = quote do:
-    return ctx.toJS(`jfcl`, `tt`)
+    return ctx.toJS(`jfcl`)
   let jsProc = gen.newJSProc(getJSGetterParams(), false)
   gen.registerFunction()
   result = newStmtList(fun, jsProc)
@@ -1033,10 +1104,9 @@ macro jsfunc*(fun: typed) =
   gen.addOptionalParams()
   gen.finishFunCallList()
   let jfcl = gen.jsFunCallList
-  let tt = gen.thisType
   gen.jsCallAndRet = if gen.returnType.issome:
     quote do:
-      return ctx.toJS(`jfcl`, `tt`)
+      return ctx.toJS(`jfcl`)
   else:
     quote do:
       `jfcl`
@@ -1126,7 +1196,7 @@ template fromJS_or_return*(t, ctx, val: untyped): untyped =
     x.get
   )
 
-macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal = false, addto = none(JSObject)): JSClassID =
+macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal = false, nointerface = false): JSClassID =
   let s = t.strVal
   var sctr = ident("js_illegal_ctor")
   var sfin = ident("js_" & s & "ClassFin")
@@ -1144,44 +1214,64 @@ macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal = fals
     let fn = $node
     result.add(quote do:
       proc `id`(ctx: JSContext, this: JSValue): JSValue =
-        if not (JS_IsUndefined(this) or ctx.isGlobal(`s`)) and not JSObject(ctx: ctx, val: this).isInstanceOf(`s`):
+        if not (JS_IsUndefined(this) or ctx.isGlobal(`s`)) and not ctx.isInstanceOf(this, `s`):
           # undefined -> global.
           return JS_ThrowTypeError(ctx, "'%s' called on an object that is not an instance of %s", `fn`, `s`)
         let arg_0 = fromJS_or_return(`t`, ctx, this)
-        return toJS(ctx, arg_0.`node`, $typeof(arg_0.`node`))
+        return toJS(ctx, arg_0.`node`)
     )
-    RegisteredFunctions[s].add((node, id))
+    if s notin RegisteredFunctions:
+      RegisteredFunctions[s] = @[(node, id)]
+    else:
+      RegisteredFunctions[s].add((node, id))
   for node in pragmas["jsset"]:
     let id = ident("js_set_" & s & "_" & $node)
     let fn = $node
     result.add(quote do:
       proc `id`(ctx: JSContext, this: JSValue, val: JSValue): JSValue =
-        var this = JSObject(ctx: ctx, val: this)
-        if not (this.isUndefined() or ctx.isGlobal(`s`)) and not this.isInstanceOf(`s`):
+        if not (JS_IsUndefined(this) or ctx.isGlobal(`s`)) and not ctx.isInstanceOf(this, `s`):
           # undefined -> global.
           return JS_ThrowTypeError(ctx, "'%s' called on an object that is not an instance of %s", `fn`, `s`)
-        let arg_0 = fromJS[`t`](this)
-        let arg_1 = JSObject(ctx: ctx, val: val)
-        arg_0.`node` = fromJS[typeof(arg_0.`node`)](arg_1)
-        return JS_DupValue(ctx, arg_1.val)
+        let arg_0 = (block:
+          let t = fromJS[`t`](ctx, this)
+          if t.isnone:
+            return JS_EXCEPTION
+          t.get
+        )
+        let arg_1 = val
+        arg_0.`node` = (block:
+          let t = fromJS[typeof(arg_0.`node`)](ctx, arg_1)
+          if t.isnone:
+            return JS_EXCEPTION
+          t.get
+        )
+        return JS_DupValue(ctx, arg_1)
     )
-    RegisteredFunctions[s].add((node, id))
-  let tabList = newNimNode(nnkBracket)
-  for fun in RegisteredFunctions[s]:
-    let f0 = fun[0]
-    let f1 = fun[1]
-    if f1.strVal.startsWith("js_new"):
-      ctorImpl = js_funcs[$f0].res
-      if ctorFun != nil:
-        error("Class " & $s & " has 2+ constructors.")
-      ctorFun = f1
-    elif f1.strVal.startsWith("js_get"):
-      getters[$f0] = f1
-    elif f1.strVal.startsWith("js_set"):
-      setters[$f0] = f1
+    if s notin RegisteredFunctions:
+      RegisteredFunctions[s] = @[(node, id)]
     else:
-      tabList.add(quote do:
-        JS_CFUNC_DEF(`f0`, 0, cast[JSCFunction](`f1`)))
+      RegisteredFunctions[s].add((node, id))
+  let tabList = newNimNode(nnkBracket)
+  if s in RegisteredFunctions:
+    for fun in RegisteredFunctions[s]:
+      #TODO this is a mess
+      var f0 = fun[0]
+      let f1 = fun[1]
+      let f2 = fun[0]
+      if f0.strVal.endsWith("_exceptions"):
+        f0 = newLit(f0.strVal.substr(0, f0.strVal.high - "_exceptions".len))
+      if f1.strVal.startsWith("js_new"):
+        ctorImpl = js_funcs[$f2].res
+        if ctorFun != nil:
+          error("Class " & $s & " has 2+ constructors.")
+        ctorFun = f1
+      elif f1.strVal.startsWith("js_get"):
+        getters[$f0] = f1
+      elif f1.strVal.startsWith("js_set"):
+        setters[$f0] = f1
+      else:
+        tabList.add(quote do:
+          JS_CFUNC_DEF(`f0`, 0, cast[JSCFunction](`f1`)))
   for k, v in getters:
     if k in setters:
       let s = setters[k]
@@ -1233,7 +1323,7 @@ macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal = fals
       var x: `t`
       new(x, nim_finalize_for_js)
       const classDef = JSClassDef(class_name: `s`, finalizer: `sfin`)
-      `ctx`.newJSClass(JSClassDefConst(unsafeAddr classDef), `sctr`, `tabList`, `parent`, `asglobal`, `addto`)
+      `ctx`.newJSClass(JSClassDefConst(unsafeAddr classDef), `sctr`, `tabList`, getTypePtr(x), `parent`, `asglobal`, `nointerface`)
   )
 
 proc getMemoryUsage*(rt: JSRuntime): string =
diff --git a/src/main.nim b/src/main.nim
index 51824798..584c92e6 100644
--- a/src/main.nim
+++ b/src/main.nim
@@ -68,6 +68,12 @@ while i < params.len:
       help(1)
   of "-h",  "--help":
     help(0)
+  of "-r", "--run":
+    inc i
+    if i < params.len:
+      gconfig.startup = params[i]
+    else:
+      help(1)
   of "--":
     escape_all = true
   elif param.len > 0:
@@ -77,7 +83,7 @@ while i < params.len:
       pages.add(param)
   inc i
 
-if pages.len == 0:
+if pages.len == 0 and gconfig.startup == "":
   if stdin.isatty:
     help(1)
 
diff --git a/src/types/url.nim b/src/types/url.nim
index 4e258b22..ef5c32ae 100644
--- a/src/types/url.nim
+++ b/src/types/url.nim
@@ -911,20 +911,57 @@ proc newURLSearchParams*[T: seq[(string, string)]|Table[string, string]|string](
 proc `$`*(params: URLSearchParams): string {.jsfunc.} =
   return serializeApplicationXWWWFormUrlEncoded(params.list)
 
+proc update(params: URLSearchParams) =
+  if params.url.isnone:
+    return
+  let serializedQuery = $params
+  if serializedQuery == "":
+    params.url.get.query = none(string)
+  else:
+    params.url.get.query = some(serializedQuery)
+
+proc append*(params: URLSearchParams, name: string, value: string) {.jsfunc.} =
+  params.list.add((name, value))
+  params.update()
+
+proc delete*(params: URLSearchParams, name: string) {.jsfunc.} =
+  for i in countdown(params.list.high, 0):
+    if params.list[i][0] == name:
+      params.list.delete(i)
+
+proc get*(params: URLSearchParams, name: string): Option[string] {.jsfunc.} =
+  for it in params.list:
+    if it[0] == name:
+      return some(it[1])
+
+proc getAll*(params: URLSearchParams, name: string): seq[string] {.jsfunc.} =
+  for it in params.list:
+    if it[0] == name:
+      result.add(it[1])
+
+proc set*(params: URLSearchParams, name: string, value: string) {.jsfunc.} =
+  var first = true
+  for i in 0..params.list.high:
+    if params.list[i][0] == name:
+      if first:
+        first = false
+        params.list[i][1] = value
+
 #TODO add Option wrapper
 proc newURL*(s: string, base: Option[string] = none(string)): URL {.jserr, jsctor.} =
   if base.issome:
     let baseUrl = parseUrl(base.get)
     if baseUrl.isnone:
-      JS_THROW TypeError, base.get & " is not a valid URL"
+      JS_THROW JS_TypeError, base.get & " is not a valid URL"
     let url = parseUrl(s, baseUrl)
     if url.isnone:
-      JS_THROW TypeError, s & " is not a valid URL"
+      JS_THROW JS_TypeError, s & " is not a valid URL"
     return url.get
   let url = parseUrl(s)
   if url.isnone:
-    JS_THROW TypeError, s & " is not a valid URL"
+    JS_THROW JS_TypeError, s & " is not a valid URL"
   url.get.searchParams = newURLSearchParams()
+  url.get.searchParams.url = url
   url.get.searchParams.initURLSearchParams(url.get.query.get(""))
   return url.get
 
@@ -1030,7 +1067,5 @@ proc hash*(url: URL, s: string) {.jsset.} =
   discard basicParseUrl(s, url = url, stateOverride = some(FRAGMENT_STATE))
 
 proc addUrlModule*(ctx: JSContext) =
-  let global = ctx.getGlobalObject()
-  discard ctx.registerType(URL, addto = some(global))
-  discard ctx.registerType(URLSearchParams, addto = some(global))
-  free(global)
+  ctx.registerType(URL)
+  ctx.registerType(URLSearchParams)
diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim
index 861edd48..41ffd09f 100644
--- a/src/utils/twtstr.nim
+++ b/src/utils/twtstr.nim
@@ -102,18 +102,16 @@ func findChar*(str: string, c: Rune, start: int = 0): int =
     i = n
   return -1
 
-func getLowerChars*(): string =
-  result = ""
+const lowerChars = (func(): array[char, char] =
   for i in 0..255:
-    if chr(i) in 'A'..'Z':
-      result &= chr(i + 32)
+    if char(i) in 'A'..'Z':
+      result[char(i)] = char(i + 32)
     else:
-      result &= chr(i)
-
-const lowerChars = getLowerChars()
+      result[char(i)] = char(i)
+)()
 
 func tolower*(c: char): char =
-  return lowerChars[int(c)]
+  return lowerChars[c]
 
 func toAsciiLower*(str: string): string =
   result = newString(str.len)
@@ -137,45 +135,31 @@ func startsWithNoCase*(str, prefix: string): bool =
     if str[i].tolower() != prefix[i].tolower(): return false
     inc i
 
-func genHexCharMap(): seq[int] =
+const hexCharMap = (func(): array[char, int] =
   for i in 0..255:
     case chr(i)
-    of '0'..'9': result &= i - ord('0')
-    of 'a'..'f': result &= i - ord('a') + 10
-    of 'A'..'F': result &= i - ord('A') + 10
-    else: result &= -1
+    of '0'..'9': result[char(i)] = i - ord('0')
+    of 'a'..'f': result[char(i)] = i - ord('a') + 10
+    of 'A'..'F': result[char(i)] = i - ord('A') + 10
+    else: result[char(i)] = -1
+)()
 
-func genDecCharMap(): seq[int] =
+const decCharMap = (func(): array[char, int] =
   for i in 0..255:
-    case chr(i)
-    of '0'..'9': result &= i - ord('0')
-    else: result &= -1
-
-const hexCharMap = genHexCharMap()
-const decCharMap = genDecCharMap()
+    case char(i)
+    of '0'..'9': result[char(i)] = i - ord('0')
+    else: result[char(i)] = -1
+)()
 
 func hexValue*(c: char): int =
-  return hexCharMap[int(c)]
+  return hexCharMap[c]
 
 func decValue*(c: char): int =
-  return decCharMap[int(c)]
-
-func isAscii*(c: char): bool =
-  return int(c) < 128
+  return decCharMap[c]
 
 func isAscii*(r: Rune): bool =
   return int(r) < 128
 
-func hexValue*(r: Rune): int =
-  if int(r) < 256:
-    return hexValue(char(r))
-  return -1
-
-func decValue*(r: Rune): int =
-  if int(r) < 256:
-    return decValue(char(r))
-  return -1
-
 const HexChars = "0123456789ABCDEF"
 func toHex*(c: char): string =
   result = newString(2)
@@ -231,14 +215,25 @@ func skipBlanks*(buf: string, at: int): int =
   while result < buf.len and buf[result].isWhitespace():
     inc result
 
-func until*(s: string, c: char): string =
+func until*(s: string, c: set[char]): string =
   var i = 0
   while i < s.len:
-    if s[i] == c:
+    if s[i] in c:
       break
     result.add(s[i])
     inc i
 
+func until*(s: string, c: char): string = s.until({c})
+
+func after*(s: string, c: set[char]): string =
+  var i = 0
+  while i < s.len:
+    if s[i] in c:
+      return s.substr(i + 1)
+    inc i
+
+func after*(s: string, c: char): string = s.after({c})
+
 func number_additive*(i: int, range: HSlice[int, int], symbols: openarray[(int, string)]): string =
   if i notin range:
     return $i
@@ -465,7 +460,7 @@ else:
 
 proc percentEncode*(append: var string, c: char, set: set[char], spaceAsPlus = false) {.inline.} =
   if spaceAsPlus and c == ' ':
-    append &= c
+    append &= '+'
   elif c notin set:
     append &= c
   else:
@@ -474,13 +469,13 @@ proc percentEncode*(append: var string, c: char, set: set[char], spaceAsPlus = f
 
 proc percentEncode*(append: var string, s: string, set: set[char], spaceAsPlus = false) {.inline.} =
   for c in s:
-    append.percentEncode(c, set)
+    append.percentEncode(c, set, spaceAsPlus)
 
-func percentEncode*(c: char, set: set[char]): string {.inline.} =
-  result.percentEncode(c, set)
+func percentEncode*(c: char, set: set[char], spaceAsPlus = false): string {.inline.} =
+  result.percentEncode(c, set, spaceAsPlus)
 
-func percentEncode*(s: string, set: set[char]): string =
-  result.percentEncode(s, set)
+func percentEncode*(s: string, set: set[char], spaceAsPlus = false): string =
+  result.percentEncode(s, set, spaceAsPlus)
 
 func percentDecode*(input: string): string =
   var i = 0