about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-07-18 18:22:15 +0200
committerbptato <nincsnevem662@gmail.com>2024-07-18 18:22:56 +0200
commitf94b84bde340739647ffe7dd92cfcefb6686eca2 (patch)
tree6e4af17196945d39ff8a1f9ab5e3516f9c0a2514
parent01cb95c89fb53a936172412e6dd69b943bf73b4b (diff)
downloadchawan-f94b84bde340739647ffe7dd92cfcefb6686eca2.tar.gz
html: event cleanup, XHR progress
-rw-r--r--src/html/catom.nim42
-rw-r--r--src/html/chadombuilder.nim4
-rw-r--r--src/html/dom.nim149
-rw-r--r--src/html/env.nim10
-rw-r--r--src/html/event.nim46
-rw-r--r--src/html/xmlhttprequest.nim305
-rw-r--r--src/loader/headers.nim49
-rw-r--r--src/loader/response.nim25
-rw-r--r--src/server/buffer.nim128
-rw-r--r--src/utils/twtstr.nim27
-rw-r--r--test/js/click_setter.html13
-rw-r--r--test/js/xhr.html17
12 files changed, 617 insertions, 198 deletions
diff --git a/src/html/catom.nim b/src/html/catom.nim
index 7c61e23e..b6dced60 100644
--- a/src/html/catom.nim
+++ b/src/html/catom.nim
@@ -4,6 +4,11 @@ import std/sets
 import std/strutils
 
 import chame/tags
+import monoucha/fromjs
+import monoucha/javascript
+import monoucha/jserror
+import monoucha/tojs
+import types/opt
 
 # create a static enum compatible with chame/tags
 
@@ -11,6 +16,7 @@ macro makeStaticAtom =
   # declare inside the macro to avoid confusion with StaticAtom0
   type
     StaticAtom0 = enum
+      satAbort = "abort"
       satAcceptCharset = "accept-charset"
       satAction = "action"
       satAlign = "align"
@@ -23,14 +29,17 @@ macro makeStaticAtom =
       satChecked = "checked"
       satClass = "class"
       satClassList = "classList"
+      satClick = "click"
       satColor = "color"
       satCols = "cols"
       satColspan = "colspan"
       satCrossorigin = "crossorigin"
+      satDOMContentLoaded = "DOMContentLoaded"
       satDefer = "defer"
       satDirname = "dirname"
       satDisabled = "disabled"
       satEnctype = "enctype"
+      satError = "error"
       satEvent = "event"
       satFor = "for"
       satForm = "form"
@@ -43,16 +52,22 @@ macro makeStaticAtom =
       satIntegrity = "integrity"
       satIsmap = "ismap"
       satLanguage = "language"
-      satMax = "max",
+      satLoad = "load"
+      satLoadend = "loadend"
+      satLoadstart = "loadstart"
+      satMax = "max"
       satMedia = "media"
       satMethod = "method"
-      satMin = "min",
+      satMin = "min"
+      satMousewheel = "mousewheel"
       satMultiple = "multiple"
       satName = "name"
       satNomodule = "nomodule"
       satNovalidate = "novalidate"
       satOnclick = "onclick"
       satOnload = "onload"
+      satProgress = "progress"
+      satReadystatechange = "readystatechange"
       satReferrerpolicy = "referrerpolicy"
       satRel = "rel"
       satRequired = "required"
@@ -68,11 +83,15 @@ macro makeStaticAtom =
       satStylesheet = "stylesheet"
       satTarget = "target"
       satText = "text"
+      satTimeout = "timeout"
       satTitle = "title"
+      satTouchmove = "touchmove"
+      satTouchstart = "touchstart"
       satType = "type"
       satUsemap = "usemap"
       satValign = "valign"
       satValue = "value"
+      satWheel = "wheel"
       satWidth = "width"
   let decl = quote do:
     type StaticAtom* {.inject.} = enum
@@ -177,3 +196,22 @@ func toStaticAtom*(factory: CAtomFactory; atom: CAtom): StaticAtom =
   if i <= int(StaticAtom.high):
     return StaticAtom(i)
   return atUnknown
+
+var getFactory* {.compileTime.}: proc(ctx: JSContext): CAtomFactory {.nimcall,
+  noSideEffect.}
+
+proc toAtom*(ctx: JSContext; atom: StaticAtom): CAtom =
+  return ctx.getFactory().toAtom(atom)
+
+proc toStaticAtom*(ctx: JSContext; atom: CAtom): StaticAtom =
+  return ctx.getFactory().toStaticAtom(atom)
+
+proc fromJSCAtom*(ctx: JSContext; val: JSValue): JSResult[CAtom] =
+  let s = ?fromJS[string](ctx, val)
+  return ok(ctx.getFactory().toAtom(s))
+
+proc toJS*(ctx: JSContext; atom: CAtom): JSValue =
+  return ctx.toJS(ctx.getFactory().toStr(atom))
+
+proc toJS*(ctx: JSContext; atom: StaticAtom): JSValue =
+  return ctx.toJS($atom)
diff --git a/src/html/chadombuilder.nim b/src/html/chadombuilder.nim
index 2bb9bec7..5470deb2 100644
--- a/src/html/chadombuilder.nim
+++ b/src/html/chadombuilder.nim
@@ -356,9 +356,7 @@ proc parseFromString(ctx: JSContext; parser: DOMParser; str, t: string):
     JSResult[Document] {.jsfunc.} =
   case t
   of "text/html":
-    let global = JS_GetGlobalObject(ctx)
-    let window = fromJS[Window](ctx, global).get
-    JS_FreeValue(ctx, global)
+    let window = ctx.getWindow()
     let url = if window.document != nil:
       window.document.url
     else:
diff --git a/src/html/dom.nim b/src/html/dom.nim
index 9f8c1f2c..9b092e3d 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -35,6 +35,7 @@ import monoucha/jserror
 import monoucha/jsopaque
 import monoucha/jspropenumlist
 import monoucha/jsutils
+import monoucha/quickjs
 import monoucha/tojs
 import types/blob
 import types/color
@@ -767,11 +768,11 @@ type
     of rtUlong, rtUlongGz:
       u: uint32
     of rtFunction:
-      ctype: string
+      ctype: StaticAtom
     else: discard
 
-func attrType0(s: static string): StaticAtom =
-  return parseEnum[StaticAtom](s)
+func attrType0(s: static string): StaticAtom {.compileTime.} =
+  return strictParseEnum[StaticAtom](s).get
 
 template toset(ts: openArray[TagType]): set[TagType] =
   var tags: system.set[TagType]
@@ -840,14 +841,14 @@ func makeulgz(name: static string; ts: varargs[TagType]; default = 0u32):
     u: default
   )
 
-func makef(name: static string; ts: set[TagType]; ctype: string): ReflectEntry =
+func makef(name: static string; ts: set[TagType]; ctype: static string): ReflectEntry =
   const attrname = attrType0(name)
   ReflectEntry(
     attrname: attrname,
     funcname: name,
     t: rtFunction,
     tags: ts,
-    ctype: ctype
+    ctype: attrType0(ctype)
   )
 
 const ReflectTable0 = [
@@ -891,11 +892,35 @@ func baseURL*(document: Document): URL
 proc delAttr(element: Element; i: int; keep = false)
 proc reflectAttr(element: Element; name: CAtom; value: Option[string])
 
+# For now, these are the same; on an API level however, getGlobal is guaranteed
+# to be non-null, while getWindow may return null in the future. (This is in
+# preparation for Worker support.)
+func getGlobal*(ctx: JSContext): Window =
+  let global = JS_GetGlobalObject(ctx)
+  let window = fromJS[Window](ctx, global).get
+  JS_FreeValue(ctx, global)
+  return window
+
+func getWindow*(ctx: JSContext): Window =
+  let global = JS_GetGlobalObject(ctx)
+  let window = fromJS[Window](ctx, global).get
+  JS_FreeValue(ctx, global)
+  return window
+
 func document*(node: Node): Document =
   if node of Document:
     return Document(node)
   return node.internalDocument
 
+proc toAtom*(window: Window; atom: StaticAtom): CAtom =
+  return window.factory.toAtom(atom)
+
+proc toAtom*(window: Window; s: string): CAtom =
+  return window.factory.toAtom(s)
+
+proc toStr*(window: Window; atom: CAtom): string =
+  return window.factory.toStr(atom)
+
 proc toAtom*(document: Document; s: string): CAtom =
   return document.factory.toAtom(s)
 
@@ -1029,6 +1054,12 @@ iterator ancestors*(node: Node): Element {.inline.} =
     yield element
     element = element.parentElement
 
+iterator nodeAncestors*(node: Node): Node {.inline.} =
+  var node = node.parentNode
+  while node != nil:
+    yield node
+    node = node.parentNode
+
 # Returns the node itself and its ancestors
 iterator branch*(node: Node): Node {.inline.} =
   var node = node
@@ -2160,7 +2191,7 @@ func serializesAsVoid(element: Element): bool =
   const Extra = {TAG_BASEFONT, TAG_BGSOUND, TAG_FRAME, TAG_KEYGEN, TAG_PARAM}
   return element.tagType in VoidElements + Extra
 
-func serializeFragment(node: Node): string
+func serializeFragment*(node: Node): string
 
 func serializeFragmentInner(child: Node; parentType: TagType): string =
   result = ""
@@ -2198,7 +2229,7 @@ func serializeFragmentInner(child: Node; parentType: TagType): string =
   elif child of DocumentType:
     result &= "<!DOCTYPE " & DocumentType(child).name & '>'
 
-func serializeFragment(node: Node): string =
+func serializeFragment*(node: Node): string =
   var node = node
   var parentType = TAG_UNKNOWN
   if node of Element:
@@ -2416,6 +2447,54 @@ isDefaultPassive = func(eventTarget: EventTarget): bool =
     EventTarget(node.document.html) == eventTarget or
     EventTarget(node.document.body) == eventTarget
 
+getFactory = proc(ctx: JSContext): CAtomFactory =
+  return ctx.getGlobal().factory
+
+type DispatchEventResult = tuple
+  called: bool
+  canceled: bool
+
+proc dispatchEvent0(window: Window; event: Event; currentTarget: EventTarget;
+    called, stop, canceled: var bool) =
+  let ctx = window.jsctx
+  event.currentTarget = currentTarget
+  var els = currentTarget.eventListeners # copy intentionally
+  for el in els:
+    if el.ctype == event.ctype:
+      let e = ctx.invoke(el, event)
+      called = true
+      if JS_IsException(e):
+        window.console.error(ctx.getExceptionMsg())
+      JS_FreeValue(ctx, e)
+      if efStopImmediatePropagation in event.flags:
+        stop = true
+        break
+      if efStopPropagation in event.flags:
+        stop = true
+      if efCanceled in event.flags:
+        canceled = true
+
+proc dispatchEvent*(window: Window; event: Event; target: EventTarget):
+    DispatchEventResult =
+  #TODO this is far from being compliant
+  var called = false
+  var canceled = false
+  let ctx = window.jsctx
+  var jsEvent = ctx.toJS(event)
+  var stop = false
+  window.dispatchEvent0(event, target, called, stop, canceled)
+  if not stop and target of Node:
+    for a in Node(target).nodeAncestors:
+      window.dispatchEvent0(event, a, called, stop, canceled)
+      if stop:
+        break
+  JS_FreeValue(ctx, jsEvent)
+  return (called, canceled)
+
+proc fireEvent*(window: Window; name: StaticAtom; target: EventTarget) =
+  let event = newEvent(window.toAtom(name), target)
+  discard window.dispatchEvent(event, target)
+
 proc parseColor(element: Element; s: string): ARGBColor =
   let cval = parseComponentValue(s)
   #TODO return element style
@@ -2685,10 +2764,7 @@ func newDocument*(factory: CAtomFactory): Document =
   return document
 
 func newDocument(ctx: JSContext): Document {.jsctor.} =
-  let global = JS_GetGlobalObject(ctx)
-  let window = fromJS[Window](ctx, global).get
-  JS_FreeValue(ctx, global)
-  return newDocument(window.factory)
+  return newDocument(ctx.getGlobal().factory)
 
 func newDocumentType*(document: Document; name, publicId, systemId: string):
     DocumentType =
@@ -3013,8 +3089,8 @@ proc loadResource(window: Window; image: HTMLImageElement) =
       )
     window.loadingResourcePromises.add(p)
 
-proc reflectEvent(element: Element; target: EventTarget; name: StaticAtom;
-    ctype, value: string) =
+proc reflectEvent(element: Element; target: EventTarget; name, ctype: StaticAtom;
+    value: string) =
   let document = element.document
   let ctx = document.window.jsctx
   let urls = document.baseURL.serialize(excludepassword = true)
@@ -3031,7 +3107,7 @@ proc reflectEvent(element: Element; target: EventTarget; name: StaticAtom;
     # directly here, but a wrapper function that calls fun. Currently
     # you can run removeEventListener with element.onclick, that should
     # not work.
-    doAssert ctx.addEventListener(target, ctype, fun).isSome
+    doAssert ctx.addEventListener(target, document.toAtom(ctype), fun).isSome
   JS_FreeValue(ctx, fun)
 
 proc reflectAttr(element: Element; name: CAtom; value: Option[string]) =
@@ -3074,12 +3150,12 @@ proc reflectAttr(element: Element; name: CAtom; value: Option[string]) =
       element.cachedStyle = nil
     return
   if name == satOnclick and element.scriptingEnabled:
-    element.reflectEvent(element, name, "click", value.get(""))
+    element.reflectEvent(element, name, satClick, value.get(""))
     return
   case element.tagType
   of TAG_BODY:
     if name == satOnload and element.scriptingEnabled:
-      element.reflectEvent(element.document.window, name, "load", value.get(""))
+      element.reflectEvent(element.document.window, name, satLoad, value.get(""))
       return
   of TAG_INPUT:
     let input = HTMLInputElement(element)
@@ -4216,19 +4292,23 @@ proc jsReflectGet(ctx: JSContext; this: JSValue; magic: cint): JSValue
   if element.tagType notin entry.tags:
     return JS_ThrowTypeError(ctx, "Invalid tag type %s", element.tagType)
   case entry.t
-  of rtStr:
-    let x = toJS(ctx, element.attr(entry.attrname))
-    return x
-  of rtBool:
-    return toJS(ctx, element.attrb(entry.attrname))
-  of rtLong:
-    return toJS(ctx, element.attrl(entry.attrname).get(entry.i))
-  of rtUlong:
-    return toJS(ctx, element.attrul(entry.attrname).get(entry.u))
-  of rtUlongGz:
-    return toJS(ctx, element.attrulgz(entry.attrname).get(entry.u))
+  of rtStr: return ctx.toJS(element.attr(entry.attrname))
+  of rtBool: return ctx.toJS(element.attrb(entry.attrname))
+  of rtLong: return ctx.toJS(element.attrl(entry.attrname).get(entry.i))
+  of rtUlong: return ctx.toJS(element.attrul(entry.attrname).get(entry.u))
+  of rtUlongGz: return ctx.toJS(element.attrulgz(entry.attrname).get(entry.u))
   of rtFunction:
-    return JS_GetPropertyStr(ctx, this, cstring($entry.attrname))
+    let val = ctx.toJS(entry.attrname)
+    let atom = JS_ValueToAtom(ctx, val)
+    var res = JS_NULL
+    var desc: JSPropertyDescriptor
+    if JS_GetOwnProperty(ctx, addr desc, this, atom) > 0:
+      JS_FreeValue(ctx, desc.setter)
+      JS_FreeValue(ctx, desc.getter)
+      res = desc.value
+    JS_FreeValue(ctx, val)
+    JS_FreeAtom(ctx, atom)
+    return res
 
 proc jsReflectSet(ctx: JSContext; this, val: JSValue; magic: cint): JSValue
     {.cdecl.} =
@@ -4272,10 +4352,12 @@ proc jsReflectSet(ctx: JSContext; this, val: JSValue; magic: cint): JSValue
       let target = fromJS[EventTarget](ctx, this).get
       ctx.definePropertyC(this, $entry.attrname, JS_DupValue(ctx, val))
       #TODO I haven't checked but this might also be wrong
-      doAssert ctx.addEventListener(target, entry.ctype, val).isSome
+      let ctype = ctx.getGlobal().toAtom(entry.ctype)
+      doAssert ctx.addEventListener(target, ctype, val).isSome
   return JS_DupValue(ctx, val)
 
 func getReflectFunctions(tags: set[TagType]): seq[TabGetSet] =
+  result = @[]
   for tag in tags:
     if tag in TagReflectMap:
       for i in TagReflectMap[tag]:
@@ -4285,7 +4367,6 @@ func getReflectFunctions(tags: set[TagType]): seq[TabGetSet] =
           set: jsReflectSet,
           magic: i
         ))
-  return result
 
 func getElementReflectFunctions(): seq[TabGetSet] =
   result = @[]
@@ -4455,13 +4536,13 @@ proc insertAdjacentHTML(element: Element; position, text: string):
 
 proc registerElements(ctx: JSContext; nodeCID: JSClassID) =
   let elementCID = ctx.registerType(Element, parent = nodeCID)
-  const extra_getset = getElementReflectFunctions()
+  const extraGetSet = getElementReflectFunctions()
   let htmlElementCID = ctx.registerType(HTMLElement, parent = elementCID,
-    has_extra_getset = true, extra_getset = extra_getset)
+    hasExtraGetSet = true, extraGetSet = extraGetSet)
   template register(t: typed; tags: set[TagType]) =
-    const extra_getset = getReflectFunctions(tags)
+    const extraGetSet = getReflectFunctions(tags)
     ctx.registerType(t, parent = htmlElementCID,
-      has_extra_getset = true, extra_getset = extra_getset)
+      hasExtraGetSet = true, extraGetSet = extra_getset)
   template register(t: typed; tag: TagType) =
     register(t, {tag})
   register(HTMLInputElement, TAG_INPUT)
diff --git a/src/html/env.nim b/src/html/env.nim
index 6b70c74f..6f994369 100644
--- a/src/html/env.nim
+++ b/src/html/env.nim
@@ -109,6 +109,9 @@ proc fetch[T: JSRequest|string](window: Window; input: T;
     return ok(promise)
   return ok(window.loader.fetch(input.request))
 
+# Forward declaration hack
+windowFetch = fetch
+
 proc setTimeout(window: Window; handler: JSValue; timeout = 0i32): int32
     {.jsfunc.} =
   return window.timeouts.setTimeout(ttTimeout, handler, timeout)
@@ -178,7 +181,7 @@ proc setOnLoad(ctx: JSContext; window: Window; val: JSValue)
     let this = ctx.toJS(window)
     ctx.definePropertyC(this, "onload", JS_DupValue(ctx, val))
     #TODO I haven't checked but this might also be wrong
-    doAssert ctx.addEventListener(window, "load", val).isSome
+    doAssert ctx.addEventListener(window, window.toAtom(satLoad), val).isSome
     JS_FreeValue(ctx, this)
 
 proc addWindowModule*(ctx: JSContext) =
@@ -237,8 +240,8 @@ proc runJSJobs*(window: Window) =
     ctx.writeException(window.console.err)
 
 proc newWindow*(scripting, images, styling: bool; selector: Selector[int];
-    attrs: WindowAttributes; factory: CAtomFactory; navigate: proc(url: URL);
-    loader: FileLoader; url: URL): Window =
+    attrs: WindowAttributes; factory: CAtomFactory; loader: FileLoader;
+    url: URL): Window =
   let err = newDynFileStream(stderr)
   let window = Window(
     attrs: attrs,
@@ -251,7 +254,6 @@ proc newWindow*(scripting, images, styling: bool; selector: Selector[int];
       scripting: scripting,
       origin: url.origin
     ),
-    navigate: navigate,
     factory: factory
   )
   window.location = window.newLocation()
diff --git a/src/html/event.nim b/src/html/event.nim
index 62d85b00..3d2a3e3a 100644
--- a/src/html/event.nim
+++ b/src/html/event.nim
@@ -2,6 +2,7 @@ import std/math
 import std/options
 import std/times
 
+import html/catom
 import monoucha/fromjs
 import monoucha/javascript
 import monoucha/jserror
@@ -28,7 +29,7 @@ type
     efDispatch
 
   Event* = ref object of RootObj
-    ctype {.jsget: "type".}: string
+    ctype* {.jsget: "type".}: CAtom
     target* {.jsget.}: EventTarget
     currentTarget* {.jsget.}: EventTarget
     eventPhase {.jsget.}: uint16
@@ -50,7 +51,7 @@ type
   EventListenerCallback = JSValue
 
   EventListener* = ref object
-    ctype*: string
+    ctype*: CAtom
     callback*: EventListenerCallback
     capture: bool
     passive: Option[bool]
@@ -68,7 +69,7 @@ jsDestructor(EventTarget)
 var isDefaultPassive*: proc(eventTarget: EventTarget): bool {.nimcall.} = nil
 
 type
-  EventInit = object of JSDict
+  EventInit* = object of JSDict
     bubbles: bool
     cancelable: bool
     composed: bool
@@ -77,7 +78,7 @@ type
     detail: JSValue
 
 # Event
-proc innerEventCreationSteps(event: Event; eventInitDict: EventInit) =
+proc innerEventCreationSteps*(event: Event; eventInitDict: EventInit) =
   event.flags = {efInitialized}
   #TODO this is probably incorrect?
   # I think it measures the time since the first fork. not sure though
@@ -88,21 +89,20 @@ proc innerEventCreationSteps(event: Event; eventInitDict: EventInit) =
     event.flags.incl(efComposed)
 
 #TODO eventInitDict type
-proc newEvent(ctype: string; eventInitDict = EventInit()):
+proc newEvent(ctx: JSContext; ctype: CAtom; eventInitDict = EventInit()):
     JSResult[Event] {.jsctor.} =
-  let event = Event()
+  let event = Event(ctype: ctype)
   event.innerEventCreationSteps(eventInitDict)
-  event.ctype = ctype
   return ok(event)
 
-proc newEvent*(ctx: JSContext; ctype: string; target: EventTarget): Event =
+proc newEvent*(ctype: CAtom; target: EventTarget): Event =
   return Event(
     ctype: ctype,
     target: target,
     currentTarget: target
   )
 
-proc initialize(this: Event; ctype: string; bubbles, cancelable: bool) =
+proc initialize(this: Event; ctype: CAtom; bubbles, cancelable: bool) =
   this.flags.incl(efInitialized)
   this.isTrusted = false
   this.target = nil
@@ -110,7 +110,7 @@ proc initialize(this: Event; ctype: string; bubbles, cancelable: bool) =
   this.bubbles = bubbles
   this.cancelable = cancelable
 
-proc initEvent(this: Event; ctype: string; bubbles, cancelable: bool)
+proc initEvent(this: Event; ctype: CAtom; bubbles, cancelable: bool)
     {.jsfunc.} =
   if efDispatch notin this.flags:
     this.initialize(ctype, bubbles, cancelable)
@@ -158,7 +158,7 @@ func composed(this: Event): bool {.jsfget.} =
   return efComposed in this.flags
 
 # CustomEvent
-proc newCustomEvent(ctype: string; eventInitDict = CustomEventInit()):
+proc newCustomEvent(ctype: CAtom; eventInitDict = CustomEventInit()):
     JSResult[CustomEvent] {.jsctor.} =
   let event = CustomEvent()
   event.innerEventCreationSteps(eventInitDict)
@@ -169,7 +169,7 @@ proc newCustomEvent(ctype: string; eventInitDict = CustomEventInit()):
 proc finalize(rt: JSRuntime; this: CustomEvent) {.jsfin.} =
   JS_FreeValueRT(rt, this.detail)
 
-proc initCustomEvent(this: CustomEvent; ctype: string;
+proc initCustomEvent(this: CustomEvent; ctype: CAtom;
     bubbles, cancelable: bool; detail: JSValue) {.jsfunc.} =
   if efDispatch notin this.flags:
     this.initialize(ctype, bubbles, cancelable)
@@ -179,15 +179,16 @@ proc initCustomEvent(this: CustomEvent; ctype: string;
 proc newEventTarget(): EventTarget {.jsctor.} =
   return EventTarget()
 
-proc defaultPassiveValue(ctype: string; eventTarget: EventTarget): bool =
-  if ctype in ["touchstart", "touchmove", "wheel", "mousewheel"]:
+proc defaultPassiveValue(ctx: JSContext; ctype: CAtom;
+    eventTarget: EventTarget): bool =
+  const check = [satTouchstart, satTouchmove, satWheel, satMousewheel]
+  if ctx.toStaticAtom(ctype) in check:
     return true
   return eventTarget.isDefaultPassive()
 
-proc findEventListener(eventTarget: EventTarget; ctype: string;
+proc findEventListener(eventTarget: EventTarget; ctype: CAtom;
     callback: EventListenerCallback; capture: bool): int =
-  for i in 0 ..< eventTarget.eventListeners.len:
-    let it = eventTarget.eventListeners[i]
+  for i, it in eventTarget.eventListeners:
     if it.ctype == ctype and it.callback == callback and it.capture == capture:
       return i
   return -1
@@ -218,12 +219,13 @@ proc invoke*(ctx: JSContext; listener: EventListener; event: Event):
   return ret
 
 # shared
-proc addAnEventListener(target: EventTarget; listener: EventListener) =
+proc addAnEventListener(ctx: JSContext; target: EventTarget;
+    listener: EventListener) =
   #TODO signals
   if JS_IsUndefined(listener.callback):
     return
   if listener.passive.isNone:
-    listener.passive = some(defaultPassiveValue(listener.ctype, target))
+    listener.passive = some(ctx.defaultPassiveValue(listener.ctype, target))
   if target.findEventListener(listener.ctype, listener.callback,
       listener.capture) == -1: # dedup
     target.eventListeners.add(listener)
@@ -267,7 +269,7 @@ proc flattenMore(ctx: JSContext; options: JSValue):
       passive = some(x.get)
   return (capture, once, passive)
 
-proc addEventListener*(ctx: JSContext; eventTarget: EventTarget; ctype: string;
+proc addEventListener*(ctx: JSContext; eventTarget: EventTarget; ctype: CAtom;
     callback: EventListenerCallback; options = JS_UNDEFINED): Err[JSError]
     {.jsfunc.} =
   if not JS_IsObject(callback) and not JS_IsNull(callback):
@@ -280,11 +282,11 @@ proc addEventListener*(ctx: JSContext; eventTarget: EventTarget; ctype: string;
     once: once,
     callback: JS_DupValue(ctx, callback)
   )
-  eventTarget.addAnEventListener(listener)
+  ctx.addAnEventListener(eventTarget, listener)
   ok()
 
 proc removeEventListener(ctx: JSContext; eventTarget: EventTarget;
-    ctype: string; callback: EventListenerCallback;
+    ctype: CAtom; callback: EventListenerCallback;
     options = JS_UNDEFINED) {.jsfunc.} =
   let capture = flatten(ctx, options)
   let i = eventTarget.findEventListener(ctype, callback, capture)
diff --git a/src/html/xmlhttprequest.nim b/src/html/xmlhttprequest.nim
index 92065ab4..2ab6b88d 100644
--- a/src/html/xmlhttprequest.nim
+++ b/src/html/xmlhttprequest.nim
@@ -1,17 +1,24 @@
 import std/options
 import std/strutils
+import std/tables
 
+import html/catom
 import html/dom
 import html/event
+import io/promise
 import js/domexception
 import loader/headers
+import loader/loader
 import loader/request
 import loader/response
 import monoucha/fromjs
 import monoucha/javascript
+import monoucha/jserror
 import monoucha/quickjs
+import monoucha/tojs
 import types/opt
 import types/url
+import utils/twtstr
 
 type
   XMLHttpRequestResponseType = enum
@@ -23,47 +30,62 @@ type
     xhrtText = "text"
 
   XMLHttpRequestState = enum
-    UNSENT = 0u16
-    OPENED = 1u16
-    HEADERS_RECEIVED = 2u16
-    LOADING = 3u16
-    DONE = 4u16
+    xhrsUnsent = (0u16, "UNSENT")
+    xhrsOpened = (1u16, "OPENED")
+    xhrsHeadersReceived = (2u16, "HEADERS_RECEIVED")
+    xhrsLoading = (3u16, "LOADING")
+    xhrsDone = (4u16, "DONE")
 
   XMLHttpRequestFlag = enum
-    xhrfSend, xhrfUploadListener, xhrfSync
+    xhrfSend, xhrfUploadListener, xhrfSync, xhrfUploadComplete, xhrfTimedOut
 
   XMLHttpRequestEventTarget = ref object of EventTarget
-    onloadstart {.jsgetset.}: EventHandler
-    onprogress {.jsgetset.}: EventHandler
-    onabort {.jsgetset.}: EventHandler
-    onerror {.jsgetset.}: EventHandler
-    onload {.jsgetset.}: EventHandler
-    ontimeout {.jsgetset.}: EventHandler
-    onloadend {.jsgetset.}: EventHandler
 
   XMLHttpRequestUpload = ref object of XMLHttpRequestEventTarget
 
   XMLHttpRequest = ref object of XMLHttpRequestEventTarget
-    onreadystatechange {.jsgetset.}: EventHandler
     readyState: XMLHttpRequestState
     upload {.jsget.}: XMLHttpRequestUpload
     flags: set[XMLHttpRequestFlag]
     requestMethod: HttpMethod
     requestURL: URL
-    authorRequestHeaders: Headers
+    headers: Headers
     response: Response
-    responseType {.jsgetset.}: XMLHttpRequestResponseType
+    responseType {.jsget.}: XMLHttpRequestResponseType
+    timeout {.jsget.}: uint32
+
+  ProgressEvent = ref object of Event
+    lengthComputable {.jsget.}: bool
+    loaded {.jsget.}: uint32
+    total {.jsget.}: uint32
+
+  ProgressEventInit = object of EventInit
+    lengthComputable: bool
+    loaded: uint32
+    total: uint32
 
 jsDestructor(XMLHttpRequestEventTarget)
 jsDestructor(XMLHttpRequestUpload)
 jsDestructor(XMLHttpRequest)
+jsDestructor(ProgressEvent)
 
 func newXMLHttpRequest(): XMLHttpRequest {.jsctor.} =
   let upload = XMLHttpRequestUpload()
   return XMLHttpRequest(
     upload: upload,
-    authorRequestHeaders: newHeaders()
+    headers: newHeaders()
+  )
+
+proc newProgressEvent(ctype: CAtom; init = ProgressEventInit()): ProgressEvent
+    {.jsctor.} =
+  let event = ProgressEvent(
+    ctype: ctype,
+    lengthComputable: init.lengthComputable,
+    loaded: init.loaded,
+    total: init.total
   )
+  Event(event).innerEventCreationSteps(init)
+  return event
 
 func readyState(this: XMLHttpRequest): uint16 {.jsfget.} =
   return uint16(this.readyState)
@@ -82,22 +104,20 @@ proc parseMethod(s: string): DOMResult[HttpMethod] =
   else:
     errDOMException("Invalid method", "SyntaxError")
 
-proc open(ctx: JSContext; this: XMLHttpRequest; httpMethod, url: string):
-    Err[DOMException] {.jsfunc.} =
+#TODO the standard says that no async should be treated differently from
+# undefined. idk if (and where) this actually matters.
+proc open(ctx: JSContext; this: XMLHttpRequest; httpMethod, url: string;
+    async = true; username = ""; password = ""): Err[DOMException] {.jsfunc.} =
   let httpMethod = ?parseMethod(httpMethod)
-  let global = JS_GetGlobalObject(ctx)
-  let window = fromJS[Window](ctx, global)
-  JS_FreeValue(ctx, global)
-  let x = if window.isSome:
-    parseURL(url, some(window.get.document.baseURL))
-  else:
-    parseURL(url)
+  let global = ctx.getGlobal()
+  let x = parseURL(url, some(global.document.baseURL))
   if x.isNone:
     return errDOMException("Invalid URL", "SyntaxError")
   let parsedURL = x.get
-  #TODO async, username, password arguments
-  let async = true
-  #TODO if async is false... probably just throw.
+  if not async and ctx.getWindow() != nil and
+      (this.timeout != 0 or this.responseType != xhrtUnknown):
+    return errDOMException("Today's horoscope: don't go outside",
+      "InvalidAccessError")
   #TODO terminate fetch controller
   this.flags.excl(xhrfSend)
   this.flags.excl(xhrfUploadListener)
@@ -106,14 +126,235 @@ proc open(ctx: JSContext; this: XMLHttpRequest; httpMethod, url: string):
   else:
     this.flags.incl(xhrfSync)
   this.requestMethod = httpMethod
-  this.authorRequestHeaders = newHeaders()
+  this.headers = newHeaders()
   this.response = makeNetworkError()
   this.requestURL = parsedURL
+  #TODO response object, received bytes
+  if this.readyState != xhrsOpened:
+    this.readyState = xhrsOpened
+    global.fireEvent(satReadystatechange, this)
   return ok()
 
+proc checkOpened(this: XMLHttpRequest): DOMResult[void] =
+  if this.readyState != xhrsOpened:
+    return errDOMException("ready state was expected to be `opened'",
+      "InvalidStateError")
+  ok()
+
+proc checkSendFlag(this: XMLHttpRequest): DOMResult[void] =
+  if xhrfSend in this.flags:
+    return errDOMException("`send' flag is set", "InvalidStateError")
+  ok()
+
+proc setRequestHeader(this: XMLHttpRequest; name, value: string):
+    DOMResult[void] {.jsfunc.} =
+  ?this.checkOpened()
+  ?this.checkSendFlag()
+  if not name.isValidHeaderName() or not value.isValidHeaderValue():
+    return errDOMException("Invalid header name or value", "SyntaxError")
+  if isForbiddenHeader(name, value):
+    return ok()
+  this.headers.table[name.toHeaderCase()] = @[value]
+  ok()
+
+proc fireProgressEvent(window: Window; target: EventTarget; name: StaticAtom;
+    loaded, length: uint32) =
+  let event = newProgressEvent(window.factory.toAtom(name), ProgressEventInit(
+    loaded: loaded,
+    total: length,
+    lengthComputable: length != 0
+  ))
+  discard window.dispatchEvent(event, target)
+
+# Forward declaration hack
+var windowFetch* {.compileTime.}: proc(window: Window; input: JSRequest;
+  init = none(RequestInit)): JSResult[FetchPromise] {.nimcall.} = nil
+
+proc errorSteps(window: Window; this: XMLHttpRequest; name: StaticAtom) =
+  this.readyState = xhrsDone
+  this.response = makeNetworkError()
+  this.flags.excl(xhrfSend)
+  #TODO sync?
+  window.fireEvent(satReadystatechange, this)
+  if xhrfUploadComplete notin this.flags:
+    this.flags.incl(xhrfUploadComplete)
+    if xhrfUploadListener in this.flags:
+      window.fireProgressEvent(this.upload, name, 0, 0)
+      window.fireProgressEvent(this.upload, satLoadend, 0, 0)
+  window.fireProgressEvent(this, name, 0, 0)
+  window.fireProgressEvent(this, satLoadend, 0, 0)
+
+proc handleErrors(window: Window; this: XMLHttpRequest): DOMException =
+  if xhrfSend notin this.flags:
+    return nil
+  if xhrfTimedOut in this.flags:
+    window.errorSteps(this, satTimeout)
+    if xhrfSync in this.flags:
+      return newDOMException("XHR timed out", "TimeoutError")
+  elif rfAborted in this.response.flags:
+    window.errorSteps(this, satAbort)
+    if xhrfSync in this.flags:
+      return newDOMException("XHR aborted", "AbortError")
+  elif this.response.responseType == rtError:
+    window.errorSteps(this, satError)
+    if xhrfSync in this.flags:
+      return newDOMException("Network error in XHR", "NetworkError")
+  return nil
+
+proc send(ctx: JSContext; this: XMLHttpRequest; body = JS_NULL): DOMResult[void]
+    {.jsfunc.} =
+  ?this.checkOpened()
+  ?this.checkSendFlag()
+  var body = body
+  if this.requestMethod in {hmGet, hmHead}:
+    body = JS_NULL
+  let request = newRequest(this.requestURL, this.requestMethod, this.headers)
+  if not JS_IsNull(body):
+    let document = fromJS[Document](ctx, body)
+    if document.isSome:
+      #TODO replace surrogates
+      let document = document.get
+      request.body = RequestBody(t: rbtString, s: document.serializeFragment())
+    #TODO else...
+    if "Content-Type" in this.headers:
+      request.headers["Content-Type"].setContentTypeAttr("charset", "UTF-8")
+    elif document.isSome:
+      request.headers["Content-Type"] = "text/html;charset=UTF-8"
+  let jsRequest = JSRequest(
+    #TODO unsafe request flag, client, cors credentials mode,
+    # use-url-credentials, initiator type
+    request: request,
+    mode: rmCors,
+    credentialsMode: cmSameOrigin,
+  )
+  if JS_IsNull(body):
+    this.flags.incl(xhrfUploadComplete)
+  else:
+    this.flags.excl(xhrfUploadComplete)
+  this.flags.excl(xhrfTimedOut)
+  this.flags.incl(xhrfSend)
+  let window = ctx.getWindow()
+  if xhrfSync notin this.flags: # async
+    window.fireProgressEvent(this, satLoadstart, 0, 0)
+    let p = window.windowFetch(jsRequest)
+    if p.isSome:
+      p.get.then(proc(res: JSResult[Response]) =
+        if res.isNone:
+          this.response = makeNetworkError()
+          discard window.handleErrors(this)
+          return
+        let response = res.get
+        this.response = response
+        this.readyState = xhrsHeadersReceived
+        window.fireEvent(satReadystatechange, this)
+      )
+  else: # sync
+    discard #TODO
+  ok()
+
+#TODO abort
+
+proc responseURL(this: XMLHttpRequest): string {.jsfget.} =
+  return this.response.surl
+
+proc status(this: XMLHttpRequest): uint16 {.jsfget.} =
+  return this.response.status
+
+proc statusText(this: XMLHttpRequest): string {.jsfget.} =
+  return ""
+
+proc getResponseHeader(this: XMLHttpRequest; name: string): string {.jsfunc.} =
+  #TODO ?
+  return this.response.headers.table.getOrDefault(name)[0]
+
+#TODO getAllResponseHeaders
+
+proc setResponseType(ctx: JSContext; this: XMLHttpRequest;
+    value: XMLHttpRequestResponseType): Err[DOMException]
+    {.jsfset: "responseType".} =
+  let window = ctx.getWindow()
+  if window == nil and value == xhrtDocument:
+    return ok()
+  if this.readyState in {xhrsLoading, xhrsDone}:
+    return errDOMException("readyState must not be loading or done",
+      "InvalidStateError")
+  if window != nil and xhrfSync in this.flags:
+    return errDOMException("responseType may not be set on synchronous XHR",
+      "InvalidAccessError")
+  this.responseType = value
+  ok()
+
+proc setTimeout(ctx: JSContext; this: XMLHttpRequest; value: uint32):
+    Err[DOMException] {.jsfset: "timeout".} =
+  if ctx.getWindow() != nil and xhrfSync in this.flags:
+    return errDOMException("timeout may not be set on synchronous XHR",
+      "InvalidAccessError")
+  this.timeout = value
+  ok()
+
+# Event reflection
+
+const ReflectMap = [
+  cint(0): satLoadstart,
+  satProgress,
+  satAbort,
+  satError,
+  satLoad,
+  satTimeout,
+  satLoadend,
+  satReadystatechange
+]
+
+proc jsReflectGet(ctx: JSContext; this: JSValue; magic: cint): JSValue
+    {.cdecl.} =
+  let val = toJS(ctx, $ReflectMap[magic])
+  let atom = JS_ValueToAtom(ctx, val)
+  var res = JS_NULL
+  var desc: JSPropertyDescriptor
+  if JS_GetOwnProperty(ctx, addr desc, this, atom) > 0:
+    JS_FreeValue(ctx, desc.setter)
+    JS_FreeValue(ctx, desc.getter)
+    res = JS_GetProperty(ctx, this, atom)
+  JS_FreeValue(ctx, val)
+  JS_FreeAtom(ctx, atom)
+  return res
+
+proc jsReflectSet(ctx: JSContext; this, val: JSValue; magic: cint): JSValue
+    {.cdecl.} =
+  if JS_IsFunction(ctx, val):
+    let atom = ReflectMap[magic]
+    let target = fromJS[EventTarget](ctx, this).get
+    ctx.definePropertyC(this, "on" & $atom, JS_DupValue(ctx, val))
+    #TODO I haven't checked but this might also be wrong
+    doAssert ctx.addEventListener(target, ctx.toAtom(atom), val).isSome
+  return JS_DupValue(ctx, val)
+
+func xhretGetSet(): seq[TabGetSet] =
+  result = @[]
+  for i, it in ReflectMap:
+    if it == satReadystatechange:
+      break
+    result.add(TabGetSet(
+      name: "on" & $it,
+      get: jsReflectGet,
+      set: jsReflectSet,
+      magic: int16(i)
+    ))
+
 proc addXMLHttpRequestModule*(ctx: JSContext) =
   let eventTargetCID = ctx.getClass("EventTarget")
-  let xhretCID = ctx.registerType(XMLHttpRequestEventTarget, eventTargetCID)
+  let eventCID = ctx.getClass("Event")
+  const getset0 = xhretGetSet()
+  let xhretCID = ctx.registerType(XMLHttpRequestEventTarget, eventTargetCID,
+    hasExtraGetSet = true, extraGetSet = getset0)
   ctx.registerType(XMLHttpRequestUpload, xhretCID)
-  let xhrCID = ctx.registerType(XMLHttpRequest, xhretCID)
+  ctx.registerType(ProgressEvent, eventCID)
+  const getset1 = [TabGetSet(
+    name: "onreadystatechange",
+    get: jsReflectGet,
+    set: jsReflectSet,
+    magic: int16(ReflectMap.high)
+  )]
+  let xhrCID = ctx.registerType(XMLHttpRequest, xhretCID, hasExtraGetSet = true,
+    extraGetSet = getset1)
   ctx.defineConsts(xhrCID, XMLHttpRequestState, uint16)
diff --git a/src/loader/headers.nim b/src/loader/headers.nim
index ff7f9ed3..cd69251c 100644
--- a/src/loader/headers.nim
+++ b/src/loader/headers.nim
@@ -1,3 +1,4 @@
+import std/strutils
 import std/tables
 
 import monoucha/fromjs
@@ -98,7 +99,7 @@ proc `[]=`*(headers: Headers; k: static string, v: string) =
   const k = k.toHeaderCase()
   headers.table[k] = @[v]
 
-func `[]`*(headers: Headers; k: static string): string =
+func `[]`*(headers: Headers; k: static string): var string =
   const k = k.toHeaderCase()
   return headers.table[k][0]
 
@@ -113,5 +114,51 @@ func getOrDefault*(headers: Headers; k: static string; default = ""): string =
   do:
     return default
 
+const TokenChars = {
+  '!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^', '_', '`', '|', '~'
+} + AsciiAlphaNumeric
+
+#TODO maybe assert these are valid on insertion?
+func isValidHeaderName*(s: string): bool =
+  return s.len > 0 and AllChars - TokenChars notin s
+
+func isValidHeaderValue*(s: string): bool =
+  return s.len == 0 or s[0] notin {' ', '\t'} and s[^1] notin {' ', '\t'} and
+    '\n' notin s
+
+func isForbiddenHeader*(name, value: string): bool =
+  const ForbiddenNames = [
+    "Accept-Charset",
+    "Accept-Encoding",
+    "Access-Control-Request-Headers",
+    "Access-Control-Request-Method",
+    "Connection",
+    "Content-Length",
+    "Cookie",
+    "Cookie2",
+    "Date",
+    "DNT",
+    "Expect",
+    "Host",
+    "Keep-Alive",
+    "Origin",
+    "Referer",
+    "Set-Cookie",
+    "TE",
+    "Trailer",
+    "Transfer-Encoding",
+    "Upgrade",
+    "Via",
+  ]
+  if name in ForbiddenNames:
+    return true
+  if name.startsWith("proxy-") or name.startsWith("sec-"):
+    return true
+  if name.equalsIgnoreCase("X-HTTP-Method") or
+      name.equalsIgnoreCase("X-HTTP-Method-Override") or
+      name.equalsIgnoreCase("X-Method-Override"):
+    return true # meh
+  return false
+
 proc addHeadersModule*(ctx: JSContext) =
   ctx.registerType(Headers)
diff --git a/src/loader/response.nim b/src/loader/response.nim
index b8d95e36..8b33621e 100644
--- a/src/loader/response.nim
+++ b/src/loader/response.nim
@@ -21,12 +21,12 @@ import utils/twtstr
 
 type
   ResponseType* = enum
-    TYPE_DEFAULT = "default"
-    TYPE_BASIC = "basic"
-    TYPE_CORS = "cors"
-    TYPE_ERROR = "error"
-    TYPE_OPAQUE = "opaque"
-    TYPE_OPAQUEREDIRECT = "opaqueredirect"
+    rtDefault = "default"
+    rtBasic = "basic"
+    rtCors = "cors"
+    rtError = "error"
+    rtOpaque = "opaque"
+    rtOpaquedirect = "opaqueredirect"
 
   #TODO fully implement headers guards
   HeadersGuard* = enum
@@ -36,6 +36,9 @@ type
     hgResponse = "response"
     hgNone = "none"
 
+  ResponseFlag* = enum
+    rfAborted
+
   Response* = ref object
     responseType* {.jsget: "type".}: ResponseType
     res*: int
@@ -52,6 +55,7 @@ type
     outputId*: int
     onRead*: proc(response: Response) {.nimcall.}
     opaque*: RootRef
+    flags*: set[ResponseFlag]
 
 jsDestructor(Response)
 
@@ -69,7 +73,7 @@ func makeNetworkError*(): Response {.jsstfunc: "Response.error".} =
   #TODO headers immutable
   return Response(
     res: 0,
-    responseType: TYPE_ERROR,
+    responseType: rtError,
     status: 0,
     headers: newHeaders(),
     headersGuard: hgImmutable,
@@ -79,8 +83,8 @@ func makeNetworkError*(): Response {.jsstfunc: "Response.error".} =
 func sok(response: Response): bool {.jsfget: "ok".} =
   return response.status in 200u16 .. 299u16
 
-func surl(response: Response): string {.jsfget: "url".} =
-  if response.responseType == TYPE_ERROR:
+func surl*(response: Response): string {.jsfget: "url".} =
+  if response.responseType == rtError or response.url == nil:
     return ""
   return $response.url
 
@@ -178,8 +182,7 @@ proc onReadBlob(response: Response) =
 proc blob*(response: Response): Promise[JSResult[Blob]] {.jsfunc.} =
   if response.bodyUsed:
     let p = newPromise[JSResult[Blob]]()
-    let err = JSResult[Blob]
-      .err(newTypeError("Body has already been consumed"))
+    let err = JSResult[Blob].err(newTypeError("Body has already been consumed"))
     p.resolve(err)
     return p
   let opaque = BlobOpaque()
diff --git a/src/server/buffer.nim b/src/server/buffer.nim
index 743fa46a..424c210f 100644
--- a/src/server/buffer.nim
+++ b/src/server/buffer.nim
@@ -46,7 +46,6 @@ import monoucha/javascript
 import monoucha/jsregex
 import monoucha/libregexp
 import monoucha/quickjs
-import monoucha/tojs
 import types/blob
 import types/cell
 import types/color
@@ -859,44 +858,6 @@ proc rewind(buffer: Buffer; offset: int; unregister = true): bool =
   buffer.bytesRead = offset
   return true
 
-proc setHTML(buffer: Buffer) =
-  buffer.initDecoder()
-  let factory = newCAtomFactory()
-  buffer.factory = factory
-  let navigate = if buffer.config.scripting:
-    proc(url: URL) = buffer.navigate(url)
-  else:
-    nil
-  buffer.window = newWindow(
-    buffer.config.scripting,
-    buffer.config.images,
-    buffer.config.styling,
-    buffer.selector,
-    buffer.attrs,
-    factory,
-    navigate,
-    buffer.loader,
-    buffer.url
-  )
-  let confidence = if buffer.config.charsetOverride == CHARSET_UNKNOWN:
-    ccTentative
-  else:
-    ccCertain
-  buffer.htmlParser = newHTML5ParserWrapper(
-    buffer.window,
-    buffer.url,
-    buffer.factory,
-    confidence,
-    buffer.charset
-  )
-  assert buffer.htmlParser.builder.document != nil
-  const css = staticRead"res/ua.css"
-  const quirk = css & staticRead"res/quirk.css"
-  buffer.uastyle = css.parseStylesheet(factory)
-  buffer.quirkstyle = quirk.parseStylesheet(factory)
-  buffer.userstyle = parseStylesheet(buffer.config.userstyle, factory)
-  buffer.document = buffer.htmlParser.builder.document
-
 # As defined in std/selectors: this determines whether kqueue is being used.
 # On these platforms, we must not close the selector after fork, since kqueue
 # fds are not inherited after a fork.
@@ -1029,17 +990,16 @@ proc clone*(buffer: Buffer; newurl: URL): int {.proxy.} =
 
 proc dispatchDOMContentLoadedEvent(buffer: Buffer) =
   let window = buffer.window
-  if window == nil or not buffer.config.scripting:
-    return
   let ctx = window.jsctx
   let document = buffer.document
-  let event = newEvent(ctx, "DOMContentLoaded", document)
+  let adcl = window.toAtom(satDOMContentLoaded)
+  let event = newEvent(adcl, document)
   var called = false
   var els = document.eventListeners
   for el in els:
     if el.removed:
       continue
-    if el.ctype == "DOMContentLoaded":
+    if el.ctype == adcl:
       let e = ctx.invoke(el, event)
       if JS_IsException(e):
         ctx.writeException(buffer.estream)
@@ -1052,16 +1012,15 @@ proc dispatchDOMContentLoadedEvent(buffer: Buffer) =
 
 proc dispatchLoadEvent(buffer: Buffer) =
   let window = buffer.window
-  if window == nil or not buffer.config.scripting:
-    return
   let ctx = window.jsctx
-  let event = newEvent(ctx, "load", window)
+  let aload = window.toAtom(satLoad)
+  let event = newEvent(aload, window)
   var called = false
   var els = window.eventListeners
   for el in els:
     if el.removed:
       continue
-    if el.ctype == "load":
+    if el.ctype == aload:
       let e = ctx.invoke(el, event)
       if JS_IsException(e):
         ctx.writeException(buffer.estream)
@@ -1072,42 +1031,6 @@ proc dispatchLoadEvent(buffer: Buffer) =
   if called:
     buffer.do_reshape()
 
-type DispatchEventResult = tuple
-  called: bool
-  canceled: bool
-
-proc dispatchEvent(buffer: Buffer; ctype, jsName: string; elem: Element):
-    DispatchEventResult =
-  var called = false
-  var canceled = false
-  let ctx = buffer.window.jsctx
-  let event = newEvent(ctx, ctype, elem)
-  var jsEvent = ctx.toJS(event)
-  let jsNameAtom = JS_NewAtomLen(ctx, cstring(jsName), csize_t(jsName.len))
-  for a in elem.branch:
-    event.currentTarget = a
-    var stop = false
-    var els = a.eventListeners
-    for el in els:
-      if el.ctype == ctype:
-        let e = ctx.invoke(el, event)
-        called = true
-        if JS_IsException(e):
-          ctx.writeException(buffer.estream)
-        JS_FreeValue(ctx, e)
-        if efStopImmediatePropagation in event.flags:
-          stop = true
-          break
-        if efStopPropagation in event.flags:
-          stop = true
-        if efCanceled in event.flags:
-          canceled = true
-    if stop:
-      break
-  JS_FreeValue(ctx, jsEvent)
-  JS_FreeAtom(ctx, jsNameAtom)
-  return (called, canceled)
-
 proc finishLoad(buffer: Buffer): EmptyPromise =
   if buffer.state != bsLoadingPage:
     let p = EmptyPromise()
@@ -1122,7 +1045,8 @@ proc finishLoad(buffer: Buffer): EmptyPromise =
     ))
   buffer.htmlParser.finish()
   buffer.document.readyState = rsInteractive
-  buffer.dispatchDOMContentLoadedEvent()
+  if buffer.config.scripting:
+    buffer.dispatchDOMContentLoadedEvent()
   buffer.selector.unregister(buffer.fd)
   buffer.loader.unregistered.add(buffer.fd)
   buffer.loader.removeCachedItem(buffer.cacheId)
@@ -1192,7 +1116,8 @@ proc onload(buffer: Buffer) =
         buffer.do_reshape()
         buffer.state = bsLoaded
         buffer.document.readyState = rsComplete
-        buffer.dispatchLoadEvent()
+        if buffer.config.scripting:
+          buffer.dispatchLoadEvent()
         if buffer.hasTask(bcGetTitle):
           buffer.resolveTask(bcGetTitle, buffer.document.title)
         if buffer.hasTask(bcLoad):
@@ -1641,8 +1566,9 @@ proc click*(buffer: Buffer; cursorx, cursory: int): ClickResult {.proxy.} =
   var canceled = false
   let clickable = buffer.getCursorClickable(cursorx, cursory)
   if buffer.config.scripting:
-    let elem = buffer.getCursorElement(cursorx, cursory)
-    (called, canceled) = buffer.dispatchEvent("click", "onclick", elem)
+    let element = buffer.getCursorElement(cursorx, cursory)
+    let event = newEvent(buffer.window.toAtom(satClick), element)
+    (called, canceled) = buffer.window.dispatchEvent(event, element)
     if called:
       buffer.do_reshape()
   if not canceled:
@@ -1912,6 +1838,11 @@ proc launchBuffer*(config: BufferConfig; url: URL; attrs: WindowAttributes;
     ssock: ServerSocket; pstream: SocketStream; selector: Selector[int]) =
   let emptySel = Selector[int]()
   emptySel[] = selector[]
+  let factory = newCAtomFactory()
+  let confidence = if config.charsetOverride == CHARSET_UNKNOWN:
+    ccTentative
+  else:
+    ccCertain
   let buffer = Buffer(
     attrs: attrs,
     config: config,
@@ -1927,8 +1858,13 @@ proc launchBuffer*(config: BufferConfig; url: URL; attrs: WindowAttributes;
     charsetStack: charsetStack,
     cacheId: -1,
     outputId: -1,
-    emptySel: emptySel
+    emptySel: emptySel,
+    factory: factory,
+    window: newWindow(config.scripting, config.images, config.styling, selector,
+      attrs, factory, loader, url)
   )
+  if buffer.config.scripting:
+    buffer.window.navigate = proc(url: URL) = buffer.navigate(url)
   buffer.charset = buffer.charsetStack.pop()
   var r = pstream.initPacketReader()
   r.sread(buffer.loader.key)
@@ -1943,7 +1879,21 @@ proc launchBuffer*(config: BufferConfig; url: URL; attrs: WindowAttributes;
   loader.unregisterFun = proc(fd: int) =
     buffer.selector.unregister(fd)
   buffer.selector.registerHandle(buffer.rfd, {Read}, 0)
-  buffer.setHTML()
+  const css = staticRead"res/ua.css"
+  const quirk = css & staticRead"res/quirk.css"
+  buffer.initDecoder()
+  buffer.uastyle = css.parseStylesheet(factory)
+  buffer.quirkstyle = quirk.parseStylesheet(factory)
+  buffer.userstyle = parseStylesheet(buffer.config.userstyle, factory)
+  buffer.htmlParser = newHTML5ParserWrapper(
+    buffer.window,
+    buffer.url,
+    buffer.factory,
+    confidence,
+    buffer.charset
+  )
+  assert buffer.htmlParser.builder.document != nil
+  buffer.document = buffer.htmlParser.builder.document
   buffer.runBuffer()
   buffer.cleanup()
   quit(0)
diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim
index 3744d9e2..a4345251 100644
--- a/src/utils/twtstr.nim
+++ b/src/utils/twtstr.nim
@@ -612,6 +612,33 @@ proc getContentTypeAttr*(contentType, attrname: string): string =
       s &= c
   return s
 
+proc setContentTypeAttr*(contentType: var string; attrname, value: string) =
+  var i = contentType.find(';')
+  if i == -1:
+    contentType &= ';' & attrname & '=' & value
+    return
+  i = contentType.find(attrname, i)
+  if i == -1:
+    contentType &= ';' & attrname & '=' & value
+    return
+  i = contentType.skipBlanks(i + attrname.len)
+  if i >= contentType.len or contentType[i] != '=':
+    contentType &= ';' & attrname & '=' & value
+    return
+  i = contentType.skipBlanks(i + 1)
+  var q = false
+  var j = i
+  while j < contentType.len:
+    let c = contentType[j]
+    if q:
+      q = false
+    elif c == '\\':
+      q = true
+    elif c in AsciiWhitespace + {';'}:
+      break
+    inc j
+  contentType[i..<j] = value
+
 func atob(c: char): uint8 {.inline.} =
   # see RFC 4648 table
   if c in AsciiUpperAlpha:
diff --git a/test/js/click_setter.html b/test/js/click_setter.html
new file mode 100644
index 00000000..125c8490
--- /dev/null
+++ b/test/js/click_setter.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<title>onclick setter/getter</title>
+<div id=x>Fail</div>
+<script src=asserts.js></script>
+<script>
+assert(document.getElementById("x").onclick === null);
+function myFunction() {
+	this.textContent = "hello"
+}
+document.getElementById("x").onclick = myFunction;
+assert(myFunction == document.getElementById("x").onclick);
+document.getElementById("x").textContent = "Success";
+</script>
diff --git a/test/js/xhr.html b/test/js/xhr.html
new file mode 100644
index 00000000..4bd66b95
--- /dev/null
+++ b/test/js/xhr.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<title>onclick setter/getter</title>
+<div id=x>Fail</div>
+<script src=asserts.js></script>
+<script>
+const x = new XMLHttpRequest();
+assert(x.onreadystatechange === null);
+function myFunction() {
+	console.log("change");
+}
+x.onreadystatechange = myFunction;
+assert(myFunction === x.onreadystatechange);
+assert(x.readyState === XMLHttpRequest.UNSENT);
+x.open("GET", "");
+document.getElementById("x").textContent = "Success";
+x.send();
+</script>