about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-01-15 20:48:29 +0100
committerbptato <nincsnevem662@gmail.com>2024-02-07 22:21:48 +0100
commit168bd542d989c76ce3ff09a29b8d77af448c3c12 (patch)
treee71ccd2bbce8da91caae46edf5df57f157a45d15
parent0beb8385d88c20efbd00b7f9e6ac57bf19a109c7 (diff)
downloadchawan-168bd542d989c76ce3ff09a29b8d77af448c3c12.tar.gz
Update chame
* Update chame to the latest version
* Get rid of nodeType usage
* Add atoms
* Re-implement DOM attributes
* document.write
m---------lib/chame0
-rw-r--r--src/css/cascade.nim8
-rw-r--r--src/css/match.nim9
-rw-r--r--src/css/selectorparser.nim58
-rw-r--r--src/css/sheet.nim60
-rw-r--r--src/html/catom.nim60
-rw-r--r--src/html/chadombuilder.nim371
-rw-r--r--src/html/dom.nim1022
-rw-r--r--src/html/enums.nim22
-rw-r--r--src/html/env.nim8
-rw-r--r--src/server/buffer.nim78
-rw-r--r--src/xhr/formdata.nim2
-rw-r--r--todo5
13 files changed, 1092 insertions, 611 deletions
diff --git a/lib/chame b/lib/chame
-Subproject 661017722195cb87a92ccd5123a4f919210d7a1
+Subproject 946af631dc066b4cd291fc5a96ee0db4b075866
diff --git a/src/css/cascade.nim b/src/css/cascade.nim
index f9422a19..4df558c0 100644
--- a/src/css/cascade.nim
+++ b/src/css/cascade.nim
@@ -91,7 +91,7 @@ proc calcRule(tosorts: var ToSorts, styledNode: StyledNode, rule: CSSRuleDef) =
 func calcRules(styledNode: StyledNode, sheet: CSSStylesheet): DeclarationList =
   var tosorts: ToSorts
   let elem = Element(styledNode.node)
-  for rule in sheet.genRules(elem.tagType, elem.id, elem.classList.toks):
+  for rule in sheet.genRules(elem.localName, elem.id, elem.classList.toks):
     tosorts.calcRule(styledNode, rule)
   for i in PseudoElem:
     tosorts[i].sort((proc(x, y: (int, seq[CSSDeclaration])): int =
@@ -383,12 +383,12 @@ proc applyRulesFrameInvalid(frame: CascadeFrame, ua, user: CSSStylesheet,
   else:
     assert child != nil
     if styledParent != nil:
-      if child.nodeType == ELEMENT_NODE:
+      if child of Element:
         styledChild = styledParent.newStyledElement(Element(child))
         styledParent.children.add(styledChild)
         declmap = styledChild.calcRules(ua, user, author)
         applyStyle(styledParent, styledChild, declmap)
-      elif child.nodeType == TEXT_NODE:
+      elif child of Text:
         let text = Text(child)
         styledChild = styledParent.newStyledText(text)
         styledParent.children.add(styledChild)
@@ -475,7 +475,7 @@ proc appendChildren(styledStack: var seq[CascadeFrame], frame: CascadeFrame,
     styledStack.stackAppend(frame, styledChild, PSEUDO_NEWLINE, idx)
   else:
     for i in countdown(elem.childList.high, 0):
-      if elem.childList[i].nodeType in {ELEMENT_NODE, TEXT_NODE}:
+      if elem.childList[i] of Element or elem.childList[i] of Text:
         styledStack.stackAppend(frame, styledChild, elem.childList[i], idx)
     if elem.tagType == TAG_INPUT:
       styledStack.stackAppend(frame, styledChild, PSEUDO_INPUT_TEXT, idx)
diff --git a/src/css/match.nim b/src/css/match.nim
index bf78f6b5..0d1f507f 100644
--- a/src/css/match.nim
+++ b/src/css/match.nim
@@ -6,6 +6,7 @@ import std/tables
 import css/cssparser
 import css/selectorparser
 import css/stylednode
+import html/catom
 import html/dom
 import utils/twtstr
 
@@ -148,9 +149,7 @@ func selectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem: T = n
     let elem = Element(selem.node)
   case sel.t
   of TYPE_SELECTOR:
-    return elem.tagType == sel.tag
-  of UNKNOWN_TYPE_SELECTOR:
-    return elem.localName == sel.tagstr
+    return elem.localName == sel.tag
   of CLASS_SELECTOR:
     return sel.class in elem.classList
   of ID_SELECTOR:
@@ -232,14 +231,14 @@ func selectorsMatch*[T: Element|StyledNode](elem: T, cxsel: ComplexSelector, fel
   return elem.complexSelectorMatches(cxsel, felem)
 
 proc querySelectorAll(node: Node, q: string): seq[Element] =
-  let selectors = parseSelectors(newStringStream(q))
+  let selectors = parseSelectors(newStringStream(q), node.document.factory)
   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(node: Node, q: string): Element =
-  let selectors = parseSelectors(newStringStream(q))
+  let selectors = parseSelectors(newStringStream(q), node.document.factory)
   for element in node.elements:
     if element.selectorsMatch(selectors):
       return element
diff --git a/src/css/selectorparser.nim b/src/css/selectorparser.nim
index 75f6182a..9e79a36d 100644
--- a/src/css/selectorparser.nim
+++ b/src/css/selectorparser.nim
@@ -3,14 +3,13 @@ import std/streams
 import std/strutils
 
 import css/cssparser
+import html/catom
 import utils/twtstr
 
-import chame/tags
-
 type
   SelectorType* = enum
-    TYPE_SELECTOR, UNKNOWN_TYPE_SELECTOR, ID_SELECTOR, ATTR_SELECTOR,
-    CLASS_SELECTOR, UNIVERSAL_SELECTOR, PSEUDO_SELECTOR, PSELEM_SELECTOR
+    TYPE_SELECTOR, ID_SELECTOR, ATTR_SELECTOR, CLASS_SELECTOR,
+    UNIVERSAL_SELECTOR, PSEUDO_SELECTOR, PSELEM_SELECTOR
 
   PseudoElem* = enum
     PSEUDO_NONE, PSEUDO_BEFORE, PSEUDO_AFTER,
@@ -32,6 +31,7 @@ type
     cvals: seq[CSSComponentValue]
     at: int
     failed: bool
+    factory: CAtomFactory
 
   RelationType* {.size: sizeof(int) div 2.} = enum
     RELATION_EXISTS, RELATION_EQUALS, RELATION_TOKEN, RELATION_BEGIN_DASH,
@@ -47,9 +47,9 @@ type
   Selector* = ref object # Simple selector
     case t*: SelectorType
     of TYPE_SELECTOR:
-      tag*: TagType
-    of UNKNOWN_TYPE_SELECTOR:
-      tagstr*: string
+      tag*: CAtom
+      when defined(debug):
+        tags: string
     of ID_SELECTOR:
       id*: string
     of ATTR_SELECTOR:
@@ -109,9 +109,10 @@ func `$`*(cxsel: ComplexSelector): string
 func `$`*(sel: Selector): string =
   case sel.t
   of TYPE_SELECTOR:
-    return tagName(sel.tag)
-  of UNKNOWN_TYPE_SELECTOR:
-    return sel.tagstr
+    when defined(debug):
+      return sel.tags
+    else:
+      return "tagt " & $int(sel.tag)
   of ID_SELECTOR:
     return '#' & sel.id
   of ATTR_SELECTOR:
@@ -205,7 +206,7 @@ func getSpecificity(sel: Selector): int =
       result += 1000
     of PSEUDO_WHERE: discard
     else: result += 1000
-  of TYPE_SELECTOR, UNKNOWN_TYPE_SELECTOR, PSELEM_SELECTOR:
+  of TYPE_SELECTOR, PSELEM_SELECTOR:
     result += 1
   of UNIVERSAL_SELECTOR:
     discard
@@ -242,15 +243,17 @@ template get_tok(cval: CSSComponentValue): CSSToken =
   if not (c of CSSToken): fail
   CSSToken(c)
 
-proc parseSelectorList(cvals: seq[CSSComponentValue]): SelectorList
+proc parseSelectorList(cvals: seq[CSSComponentValue], factory: CAtomFactory):
+  SelectorList
 
 # Functions that may contain other selectors, functions, etc.
-proc parseRecursiveSelectorFunction(state: var SelectorParser, class: PseudoClass, body: seq[CSSComponentValue]): Selector =
+proc parseRecursiveSelectorFunction(state: var SelectorParser,
+    class: PseudoClass, body: seq[CSSComponentValue]): Selector =
   var fun = Selector(
     t: PSEUDO_SELECTOR,
     pseudo: PseudoData(t: class),
   )
-  fun.pseudo.fsels = parseSelectorList(body)
+  fun.pseudo.fsels = parseSelectorList(body, state.factory)
   if fun.pseudo.fsels.len == 0: fail
   return fun
 
@@ -266,7 +269,8 @@ proc parseNthChild(state: var SelectorParser, cssfunction: CSSFunction, data: Ps
     return nthchild
   if not (get_tok cssfunction.value[i]).value.equalsIgnoreCase("of"): fail
   if i == cssfunction.value.len: fail
-  nthchild.pseudo.ofsels = parseSelectorList(cssfunction.value[i..^1])
+  nthchild.pseudo.ofsels = parseSelectorList(cssfunction.value[i..^1],
+    state.factory)
   if nthchild.pseudo.ofsels.len == 0: fail
   return nthchild
 
@@ -396,6 +400,7 @@ proc parseClassSelector(state: var SelectorParser): Selector =
   return Selector(t: CLASS_SELECTOR, class: tok.value)
 
 proc parseCompoundSelector(state: var SelectorParser): CompoundSelector =
+  result = CompoundSelector()
   while state.has():
     let cval = state.peek()
     if cval of CSSToken:
@@ -403,12 +408,8 @@ proc parseCompoundSelector(state: var SelectorParser): CompoundSelector =
       case tok.tokenType
       of CSS_IDENT_TOKEN:
         inc state.at
-        let s = tok.value.toLowerAscii()
-        let tag = tagType(s)
-        if tag == TAG_UNKNOWN:
-          result.add(Selector(t: UNKNOWN_TYPE_SELECTOR, tagstr: s))
-        else:
-          result.add(Selector(t: TYPE_SELECTOR, tag: tag))
+        let tag = state.factory.toAtom(tok.value.toLowerAscii())
+        result.add(Selector(t: TYPE_SELECTOR, tag: tag))
       of CSS_COLON_TOKEN:
         inc state.at
         result.add(state.parsePseudoSelector())
@@ -466,16 +467,19 @@ proc parseComplexSelector(state: var SelectorParser): ComplexSelector =
   if result.len == 0 or result[^1].ct != NO_COMBINATOR:
     fail
 
-proc parseSelectorList(cvals: seq[CSSComponentValue]): SelectorList =
-  var state = SelectorParser(cvals: cvals)
+proc parseSelectorList(cvals: seq[CSSComponentValue], factory: CAtomFactory):
+    SelectorList =
+  var state = SelectorParser(cvals: cvals, factory: factory)
   var res: SelectorList
   while state.has():
     res.add(state.parseComplexSelector())
   if not state.failed:
     return res
 
-func parseSelectors*(cvals: seq[CSSComponentValue]): seq[ComplexSelector] = {.cast(noSideEffect).}:
-  return parseSelectorList(cvals)
+proc parseSelectors*(cvals: seq[CSSComponentValue], factory: CAtomFactory):
+    seq[ComplexSelector] =
+  return parseSelectorList(cvals, factory)
 
-proc parseSelectors*(stream: Stream): seq[ComplexSelector] =
-  return parseSelectors(parseListOfComponentValues(stream))
+proc parseSelectors*(stream: Stream, factory: CAtomFactory):
+    seq[ComplexSelector] =
+  return parseSelectors(parseListOfComponentValues(stream), factory)
diff --git a/src/css/sheet.nim b/src/css/sheet.nim
index 2db7b47a..4990d991 100644
--- a/src/css/sheet.nim
+++ b/src/css/sheet.nim
@@ -2,11 +2,10 @@ import std/algorithm
 import std/streams
 import std/tables
 
-import css/mediaquery
 import css/cssparser
+import css/mediaquery
 import css/selectorparser
-
-import chame/tags
+import html/catom
 
 type
   CSSRuleBase* = ref object of RootObj
@@ -26,23 +25,27 @@ type
 
   CSSStylesheet* = ref object
     mqList*: seq[CSSMediaQueryDef]
-    tagTable: array[TagType, seq[CSSRuleDef]]
+    #TODO maybe just array[TagType] would be more efficient
+    tagTable: Table[CAtom, seq[CSSRuleDef]]
     idTable: Table[string, seq[CSSRuleDef]]
     classTable: Table[string, seq[CSSRuleDef]]
     generalList: seq[CSSRuleDef]
     len: int
+    factory: CAtomFactory
 
 type SelectorHashes = object
-  tag: TagType
+  tag: CAtom
   id: string
   class: string
 
-func newStylesheet*(cap: int): CSSStylesheet =
+func newStylesheet*(cap: int, factory: CAtomFactory): CSSStylesheet =
   let bucketsize = cap div 2
   return CSSStylesheet(
+    tagTable: initTable[CAtom, seq[CSSRuleDef]](bucketsize),
     idTable: initTable[string, seq[CSSRuleDef]](bucketsize),
     classTable: initTable[string, seq[CSSRuleDef]](bucketsize),
-    generalList: newSeqOfCap[CSSRuleDef](bucketsize)
+    generalList: newSeqOfCap[CSSRuleDef](bucketsize),
+    factory: factory
   )
 
 proc getSelectorIds(hashes: var SelectorHashes, sel: Selector): bool
@@ -66,7 +69,7 @@ proc getSelectorIds(hashes: var SelectorHashes, sel: Selector): bool =
   of ID_SELECTOR:
     hashes.id = sel.id
     return true
-  of ATTR_SELECTOR, PSELEM_SELECTOR, UNIVERSAL_SELECTOR, UNKNOWN_TYPE_SELECTOR:
+  of ATTR_SELECTOR, PSELEM_SELECTOR, UNIVERSAL_SELECTOR:
     return false
   of PSEUDO_SELECTOR:
     if sel.pseudo.t in {PSEUDO_IS, PSEUDO_WHERE}:
@@ -88,9 +91,9 @@ proc getSelectorIds(hashes: var SelectorHashes, sel: Selector): bool =
       while i < sel.pseudo.fsels.len:
         var nhashes: SelectorHashes
         nhashes.getSelectorIds(sel.pseudo.fsels[i])
-        if hashes.tag == TAG_UNKNOWN:
+        if hashes.tag == CAtomNull:
           hashes.tag = nhashes.tag
-        elif not cancel_tag and nhashes.tag != TAG_UNKNOWN and nhashes.tag != hashes.tag:
+        elif not cancel_tag and nhashes.tag != CAtomNull and nhashes.tag != hashes.tag:
           cancel_tag = true
 
         if hashes.id == "":
@@ -106,23 +109,24 @@ proc getSelectorIds(hashes: var SelectorHashes, sel: Selector): bool =
         inc i
 
       if cancel_tag:
-        hashes.tag = TAG_UNKNOWN
+        hashes.tag = CAtomNull
       if cancel_id:
         hashes.id = ""
       if cancel_class:
         hashes.class = ""
 
-      if hashes.tag != TAG_UNKNOWN or hashes.id != "" or hashes.class != "":
+      if hashes.tag != CAtomNull or hashes.id != "" or hashes.class != "":
         return true
 
 proc ruleDefCmp(a, b: CSSRuleDef): int =
   cmp(a.idx, b.idx)
 
-iterator genRules*(sheet: CSSStylesheet, tag: TagType, id: string,
+iterator genRules*(sheet: CSSStylesheet, tag: CAtom, id: string,
     classes: seq[string]): CSSRuleDef =
   var rules: seq[CSSRuleDef]
-  for rule in sheet.tagTable[tag]:
-    rules.add(rule)
+  sheet.tagTable.withValue(tag, v):
+    for rule in v[]:
+      rules.add(rule)
   if id != "":
     sheet.idTable.withValue(id, v):
       for rule in v[]:
@@ -141,8 +145,11 @@ proc add(sheet: var CSSStylesheet, rule: CSSRuleDef) =
   var hashes: SelectorHashes
   for cxsel in rule.sels:
     hashes.getSelectorIds(cxsel)
-    if hashes.tag != TAG_UNKNOWN:
-      sheet.tagTable[hashes.tag].add(rule)
+    if hashes.tag != CAtomNull:
+      sheet.tagTable.withValue(hashes.tag, p):
+        p[].add(rule)
+      do:
+        sheet.tagTable[hashes.tag] = @[rule]
     elif hashes.id != "":
       sheet.idTable.withValue(hashes.id, p):
         p[].add(rule)
@@ -158,8 +165,11 @@ proc add(sheet: var CSSStylesheet, rule: CSSRuleDef) =
 
 proc add*(sheet: var CSSStylesheet, sheet2: CSSStylesheet) =
   sheet.generalList.add(sheet2.generalList)
-  for tag in TagType:
-    sheet.tagTable[tag].add(sheet2.tagTable[tag])
+  for key, value in sheet2.tagTable.pairs:
+    sheet.tagTable.withValue(key, p):
+      p[].add(value)
+    do:
+      sheet.tagTable[key] = value
   for key, value in sheet2.idTable.pairs:
     sheet.idTable.withValue(key, p):
       p[].add(value)
@@ -172,7 +182,7 @@ proc add*(sheet: var CSSStylesheet, sheet2: CSSStylesheet) =
       sheet.classTable[key] = value
 
 proc addRule(stylesheet: var CSSStylesheet, rule: CSSQualifiedRule) =
-  let sels = parseSelectors(rule.prelude)
+  let sels = parseSelectors(rule.prelude, stylesheet.factory)
   if sels.len > 0:
     let r = CSSRuleDef(
       sels: sels,
@@ -192,7 +202,7 @@ proc addAtRule(stylesheet: var CSSStylesheet, atrule: CSSAtRule) =
     let rules = atrule.oblock.value.parseListOfRules()
     if rules.len > 0:
       var media = CSSMediaQueryDef()
-      media.children = newStylesheet(rules.len)
+      media.children = newStylesheet(rules.len, stylesheet.factory)
       media.children.len = stylesheet.len
       media.query = query
       for rule in rules:
@@ -204,13 +214,13 @@ proc addAtRule(stylesheet: var CSSStylesheet, atrule: CSSAtRule) =
       stylesheet.len = media.children.len
   else: discard #TODO
 
-proc parseStylesheet*(s: Stream): CSSStylesheet =
+proc parseStylesheet*(s: Stream, factory: CAtomFactory): CSSStylesheet =
   let css = parseCSS(s)
-  result = newStylesheet(css.value.len)
+  result = newStylesheet(css.value.len, factory)
   for v in css.value:
     if v of CSSAtRule: result.addAtRule(CSSAtRule(v))
     else: result.addRule(CSSQualifiedRule(v))
   s.close()
 
-proc parseStylesheet*(s: string): CSSStylesheet =
-  return newStringStream(s).parseStylesheet()
+proc parseStylesheet*(s: string, factory: CAtomFactory): CSSStylesheet =
+  return newStringStream(s).parseStylesheet(factory)
diff --git a/src/html/catom.nim b/src/html/catom.nim
new file mode 100644
index 00000000..62066663
--- /dev/null
+++ b/src/html/catom.nim
@@ -0,0 +1,60 @@
+import std/hashes
+
+import chame/tags
+
+#TODO use a better hash map
+const CAtomFactoryStrMapLength = 1024 # must be a power of 2
+static:
+  doAssert (CAtomFactoryStrMapLength and (CAtomFactoryStrMapLength - 1)) == 0
+
+type
+  CAtom* = distinct int
+
+  #TODO could be a ptr probably
+  CAtomFactory* = ref object of RootObj
+    strMap: array[CAtomFactoryStrMapLength, seq[CAtom]]
+    atomMap: seq[string]
+
+const CAtomNull* = CAtom(0)
+
+# Mandatory Atom functions
+func `==`*(a, b: CAtom): bool {.borrow.}
+func hash*(atom: CAtom): Hash {.borrow.}
+
+func toAtom*(factory: CAtomFactory, s: string): CAtom
+
+proc newCAtomFactory*(): CAtomFactory =
+  const minCap = int(TagType.high) + 1
+  let factory = CAtomFactory(
+    atomMap: newSeqOfCap[string](minCap),
+  )
+  factory.atomMap.add("") # skip TAG_UNKNOWN
+  for tagType in TagType(int(TAG_UNKNOWN) + 1) .. TagType.high:
+    discard factory.toAtom($tagType)
+  return factory
+
+func toAtom*(factory: CAtomFactory, s: string): CAtom =
+  let h = s.hash()
+  let i = h and (factory.strMap.len - 1)
+  for atom in factory.strMap[i]:
+    if factory.atomMap[int(atom)] == s:
+      # Found
+      return atom
+  # Not found
+  let atom = CAtom(factory.atomMap.len)
+  factory.atomMap.add(s)
+  factory.strMap[i].add(atom)
+  return atom
+
+func toAtom*(factory: CAtomFactory, tagType: TagType): CAtom =
+  assert tagType != TAG_UNKNOWN
+  return CAtom(tagType)
+
+func toStr*(factory: CAtomFactory, atom: CAtom): string =
+  return factory.atomMap[int(atom)]
+
+func toTagType*(factory: CAtomFactory, atom: CAtom): TagType =
+  let i = int(atom)
+  if i > 0 and i <= int(high(TagType)):
+    return TagType(atom)
+  return TAG_UNKNOWN
diff --git a/src/html/chadombuilder.nim b/src/html/chadombuilder.nim
index fa3b761d..21e94901 100644
--- a/src/html/chadombuilder.nim
+++ b/src/html/chadombuilder.nim
@@ -2,6 +2,7 @@ import std/deques
 import std/options
 import std/streams
 
+import html/catom
 import html/dom
 import html/enums
 import js/error
@@ -10,138 +11,167 @@ import js/javascript
 import types/url
 
 import chakasu/charset
+import chakasu/decoderstream
+import chakasu/encoderstream
 
 import chame/htmlparser
-import chame/htmltokenizer
 import chame/tags
 
 # DOMBuilder implementation for Chawan.
 
+type CharsetConfidence = enum
+  CONFIDENCE_TENTATIVE, CONFIDENCE_CERTAIN, CONFIDENCE_IRRELEVANT
+
 type
-  ChaDOMBuilder = ref object of DOMBuilder[Node]
+  ChaDOMBuilder = ref object of DOMBuilder[Node, CAtom]
+    charset: Charset
+    confidence: CharsetConfidence
+    document: Document
     isFragment: bool
+    factory: CAtomFactory
+    poppedScript: HTMLScriptElement
+
+type
+  DOMBuilderImpl = ChaDOMBuilder
+  HandleImpl = Node
+  AtomImpl = CAtom
+
+include chame/htmlparseriface
 
 type DOMParser = ref object # JS interface
 
 jsDestructor(DOMParser)
 
-template getDocument(dombuilder: ChaDOMBuilder): Document =
-  cast[Document](dombuilder.document)
+proc getDocumentImpl(builder: ChaDOMBuilder): Node =
+  return builder.document
+
+proc atomToTagTypeImpl(builder: ChaDOMBuilder, atom: CAtom): TagType =
+  return builder.factory.toTagType(atom)
+
+proc tagTypeToAtomImpl(builder: ChaDOMBuilder, tagType: TagType): CAtom =
+  return builder.factory.toAtom(tagType)
 
-proc finish(builder: DOMBuilder[Node]) =
-  let builder = cast[ChaDOMBuilder](builder)
-  let document = builder.getDocument()
-  while document.scriptsToExecOnLoad.len > 0:
+proc strToAtomImpl(builder: ChaDOMBuilder, s: string): CAtom =
+  return builder.factory.toAtom(s)
+
+proc finish(builder: ChaDOMBuilder) =
+  while builder.document.scriptsToExecOnLoad.len > 0:
     #TODO spin event loop
-    let script = document.scriptsToExecOnLoad.popFirst()
+    let script = builder.document.scriptsToExecOnLoad.popFirst()
     script.execute()
   #TODO events
 
-proc restart(builder: DOMBuilder[Node]) =
-  let document = newDocument()
+proc restart(builder: ChaDOMBuilder) =
+  let document = newDocument(builder.factory)
   document.contentType = "text/html"
-  let oldDocument = cast[Document](builder.document)
+  let oldDocument = builder.document
   document.url = oldDocument.url
   let window = oldDocument.window
   if window != nil:
     document.window = window
     window.document = document
   builder.document = document
+  assert document.factory != nil
+
+proc setQuirksModeImpl(builder: ChaDOMBuilder, quirksMode: QuirksMode) =
+  if not builder.document.parser_cannot_change_the_mode_flag:
+    builder.document.mode = quirksMode
+
+proc setEncodingImpl(builder: ChaDOMBuilder, encoding: string):
+    SetEncodingResult =
+  let charset = getCharset(encoding)
+  if charset == CHARSET_UNKNOWN:
+    return SET_ENCODING_CONTINUE
+  if builder.charset in {CHARSET_UTF_16_LE, CHARSET_UTF_16_BE}:
+    builder.confidence = CONFIDENCE_CERTAIN
+    return SET_ENCODING_CONTINUE
+  builder.confidence = CONFIDENCE_CERTAIN
+  if charset == builder.charset:
+    return SET_ENCODING_CONTINUE
+  if charset == CHARSET_X_USER_DEFINED:
+    builder.charset = CHARSET_WINDOWS_1252
+  else:
+    builder.charset = charset
+  return SET_ENCODING_STOP
 
-proc parseError(builder: DOMBuilder[Node], message: string) =
-  discard
-
-proc setQuirksMode(builder: DOMBuilder[Node], quirksMode: QuirksMode) =
-  let builder = cast[ChaDOMBuilder](builder)
-  let document = builder.getDocument()
-  if not document.parser_cannot_change_the_mode_flag:
-    document.mode = quirksMode
-
-proc setCharacterSet(builder: DOMBuilder[Node], charset: Charset) =
-  let builder = cast[ChaDOMBuilder](builder)
-  let document = builder.getDocument()
-  document.charset = charset
-
-proc getTemplateContent(builder: DOMBuilder[Node], handle: Node): Node =
+proc getTemplateContentImpl(builder: ChaDOMBuilder, handle: Node): Node =
   return HTMLTemplateElement(handle).content
 
-proc getTagType(builder: DOMBuilder[Node], handle: Node): TagType =
-  return Element(handle).tagType
-
-proc getParentNode(builder: DOMBuilder[Node], handle: Node): Option[Node] =
+proc getParentNodeImpl(builder: ChaDOMBuilder, handle: Node): Option[Node] =
   return option(handle.parentNode)
 
-proc getLocalName(builder: DOMBuilder[Node], handle: Node): string =
+proc getLocalNameImpl(builder: ChaDOMBuilder, handle: Node): CAtom =
   return Element(handle).localName
 
-proc getNamespace(builder: DOMBuilder[Node], handle: Node): Namespace =
+proc getNamespaceImpl(builder: ChaDOMBuilder, handle: Node): Namespace =
   return Element(handle).namespace
 
-proc createElement(builder: DOMBuilder[Node], localName: string,
-    namespace: Namespace, tagType: TagType,
-    attrs: Table[string, string]): Node =
-  let builder = cast[ChaDOMBuilder](builder)
-  let document = builder.getDocument()
-  let element = document.newHTMLElement(localName, namespace,
-    tagType = tagType, attrs = attrs)
-  if element.isResettable():
+proc createHTMLElementImpl(builder: ChaDOMBuilder): Node =
+  return builder.document.newHTMLElement(TAG_HTML)
+
+proc createElementForTokenImpl(builder: ChaDOMBuilder, localName: CAtom,
+    namespace: Namespace, intendedParent: Node, htmlAttrs: Table[CAtom, string],
+    xmlAttrs: seq[ParsedAttr[CAtom]]): Node =
+  let document = builder.document
+  let element = document.newHTMLElement(localName, namespace)
+  for k, v in htmlAttrs:
+    element.attr(k, v)
+  for attr in xmlAttrs:
+    element.attrns(attr.name, attr.prefix, attr.namespace, attr.value)
+  if element.tagType in ResettableElements:
     element.resetElement()
-  if tagType == TAG_SCRIPT:
+  if element of HTMLScriptElement:
     let script = HTMLScriptElement(element)
     script.parserDocument = document
     script.forceAsync = false
-    if builder.isFragment:
-      script.alreadyStarted = true
-      #TODO document.write (?)
+    # Note: per standard, we could set already started to true here when we
+    # are parsing from document.write, but that sounds like a horrible idea.
   return element
 
-proc createComment(builder: DOMBuilder[Node], text: string): Node =
-  let builder = cast[ChaDOMBuilder](builder)
-  return builder.getDocument().createComment(text)
+proc createCommentImpl(builder: ChaDOMBuilder, text: string): Node =
+  return builder.document.createComment(text)
 
-proc createDocumentType(builder: DOMBuilder[Node], name, publicId,
+proc createDocumentTypeImpl(builder: ChaDOMBuilder, name, publicId,
     systemId: string): Node =
-  let builder = cast[ChaDOMBuilder](builder)
-  return builder.getDocument().newDocumentType(name, publicId, systemId)
-
-proc insertBefore(builder: DOMBuilder[Node], parent, child,
-    before: Node) =
-  discard parent.insertBefore(child, before)
-
-proc insertText(builder: DOMBuilder[Node], parent: Node, text: string,
-    before: Node) =
-  let builder = cast[ChaDOMBuilder](builder)
-  let prevSibling = if before != nil:
-    before.previousSibling
+  return builder.document.newDocumentType(name, publicId, systemId)
+
+proc insertBeforeImpl(builder: ChaDOMBuilder, parent, child: Node,
+    before: Option[Node]) =
+  discard parent.insertBefore(child, before.get(nil))
+
+proc insertTextImpl(builder: ChaDOMBuilder, parent: Node, text: string,
+    before: Option[Node]) =
+  let prevSibling = if before.isSome:
+    before.get.previousSibling
   else:
     parent.lastChild
-  if prevSibling != nil and prevSibling.nodeType == TEXT_NODE:
+  if prevSibling != nil and prevSibling of Text:
     Text(prevSibling).data &= text
   else:
-    let text = builder.getDocument().createTextNode(text)
-    discard parent.insertBefore(text, before)
+    let text = builder.document.createTextNode(text)
+    discard parent.insertBefore(text, before.get(nil))
 
-proc remove(builder: DOMBuilder[Node], child: Node) =
+proc removeImpl(builder: ChaDOMBuilder, child: Node) =
   child.remove(suppressObservers = true)
 
-proc moveChildren(builder: DOMBuilder[Node], fromNode, toNode: Node) =
+proc moveChildrenImpl(builder: ChaDOMBuilder, fromNode, toNode: Node) =
   var tomove = fromNode.childList
   for node in tomove:
     node.remove(suppressObservers = true)
   for child in tomove:
     toNode.insert(child, nil)
 
-proc addAttrsIfMissing(builder: DOMBuilder[Node], element: Node,
-    attrs: Table[string, string]) =
-  let element = Element(element)
+proc addAttrsIfMissingImpl(builder: ChaDOMBuilder, handle: Node,
+    attrs: Table[CAtom, string]) =
+  let element = Element(handle)
   for k, v in attrs:
     if not element.attrb(k):
       element.attr(k, v)
 
-proc setScriptAlreadyStarted(builder: DOMBuilder[Node], script: Node) =
+proc setScriptAlreadyStartedImpl(builder: ChaDOMBuilder, script: Node) =
   HTMLScriptElement(script).alreadyStarted = true
 
-proc associateWithForm(builder: DOMBuilder[Node], element, form,
+proc associateWithFormImpl(builder: ChaDOMBuilder, element, form,
     intendedParent: Node) =
   if form.inSameTree(intendedParent):
     #TODO remove following test eventually
@@ -150,26 +180,17 @@ proc associateWithForm(builder: DOMBuilder[Node], element, form,
       element.setForm(HTMLFormElement(form))
       element.parserInserted = true
 
-proc elementPopped(builder: DOMBuilder[Node], element: Node) =
-  let builder = cast[ChaDOMBuilder](builder)
-  let document = builder.getDocument()
+proc elementPoppedImpl(builder: ChaDOMBuilder, element: Node) =
   let element = Element(element)
-  if element.tagType == TAG_TEXTAREA:
+  if element of HTMLTextAreaElement:
     element.resetElement()
-  elif element.tagType == TAG_SCRIPT:
-    #TODO microtask (maybe it works here too?)
-    let script = HTMLScriptElement(element)
-    #TODO document.write() (?)
-    script.prepare()
-    while document.parserBlockingScript != nil:
-      let script = document.parserBlockingScript
-      document.parserBlockingScript = nil
-      #TODO style sheet
-      script.execute()
-
-proc newChaDOMBuilder(url: URL, window: Window, isFragment = false):
-    ChaDOMBuilder =
-  let document = newDocument()
+  elif element of HTMLScriptElement:
+    assert builder.poppedScript == nil or not builder.document.scriptingEnabled
+    builder.poppedScript = HTMLScriptElement(element)
+
+proc newChaDOMBuilder(url: URL, window: Window, factory: CAtomFactory,
+    isFragment = false): ChaDOMBuilder =
+  let document = newDocument(factory)
   document.contentType = "text/html"
   document.url = url
   if window != nil:
@@ -177,36 +198,18 @@ proc newChaDOMBuilder(url: URL, window: Window, isFragment = false):
     window.document = document
   return ChaDOMBuilder(
     document: document,
-    finish: finish,
-    restart: restart,
-    setQuirksMode: setQuirksMode,
-    setCharacterSet: setCharacterSet,
-    elementPopped: elementPopped,
-    getTemplateContent: getTemplateContent,
-    getTagType: getTagType,
-    getParentNode: getParentNode,
-    getLocalName: getLocalName,
-    getNamespace: getNamespace,
-    createElement: createElement,
-    createComment: createComment,
-    createDocumentType: createDocumentType,
-    insertBefore: insertBefore,
-    insertText: insertText,
-    remove: remove,
-    moveChildren: moveChildren,
-    addAttrsIfMissing: addAttrsIfMissing,
-    setScriptAlreadyStarted: setScriptAlreadyStarted,
-    associateWithForm: associateWithForm,
-    #TODO isSVGIntegrationPoint (SVG support)
-    isFragment: isFragment
+    isFragment: isFragment,
+    factory: factory
   )
 
 # https://html.spec.whatwg.org/multipage/parsing.html#parsing-html-fragments
 proc parseHTMLFragment*(element: Element, s: string): seq[Node] =
   let url = parseURL("about:blank").get
-  let builder = newChaDOMBuilder(url, nil)
+  let factory = element.document.factory
+  let builder = newChaDOMBuilder(url, nil, factory)
+  let inputStream = newStringStream(s)
   builder.isFragment = true
-  let document = Document(builder.document)
+  let document = builder.document
   document.mode = element.document.mode
   let state = case element.tagType
   of TAG_TITLE, TAG_TEXTAREA: RCDATA
@@ -222,31 +225,139 @@ proc parseHTMLFragment*(element: Element, s: string): seq[Node] =
   else: DATA
   let root = document.newHTMLElement(TAG_HTML)
   document.append(root)
-  let opts = HTML5ParserOpts[Node](
+  let opts = HTML5ParserOpts[Node, CAtom](
     isIframeSrcdoc: false, #TODO?
     scripting: false,
-    canReinterpret: false,
-    charsets: @[CHARSET_UTF_8],
-    ctx: some(Node(element)),
+    ctx: some((Node(element), element.localName)),
     initialTokenizerState: state,
-    openElementsInit: @[Node(root)],
+    openElementsInit: @[(Node(root), root.localName)],
     pushInTemplate: element.tagType == TAG_TEMPLATE
   )
-  let inputStream = newStringStream(s)
-  parseHTML(inputStream, builder, opts)
+  var parser = initHTML5Parser(builder, opts)
+  var buffer: array[4096, char]
+  while true:
+    let n = inputStream.readData(addr buffer[0], buffer.len)
+    if n == 0: break
+    let res = parser.parseChunk(buffer.toOpenArray(0, n - 1))
+    assert res == PRES_CONTINUE # scripting is false, so this must be continue
+  parser.finish()
+  builder.finish()
   return root.childList
 
+#TODO this should be handled by decoderstream
+proc bomSniff(inputStream: Stream): Charset =
+  let bom = inputStream.readStr(2)
+  if bom == "\xFE\xFF":
+    return CHARSET_UTF_16_BE
+  if bom == "\xFF\xFE":
+    return CHARSET_UTF_16_LE
+  if bom == "\xEF\xBB":
+    if inputStream.readChar() == '\xBF':
+      return CHARSET_UTF_8
+  inputStream.setPosition(0)
+  return CHARSET_UNKNOWN
+
 proc parseHTML*(inputStream: Stream, window: Window, url: URL,
-    charsets: seq[Charset] = @[], canReinterpret = true): Document =
-  let builder = newChaDOMBuilder(url, window)
-  let opts = HTML5ParserOpts[Node](
+    factory: CAtomFactory, charsets: seq[Charset] = @[],
+    seekable = true): Document =
+  let opts = HTML5ParserOpts[Node, CAtom](
     isIframeSrcdoc: false, #TODO?
-    scripting: window != nil and window.settings.scripting,
-    canReinterpret: canReinterpret,
-    charsets: charsets
+    scripting: window != nil and window.settings.scripting
   )
-  parseHTML(inputStream, builder, opts)
-  return Document(builder.document)
+  let builder = newChaDOMBuilder(url, window, factory)
+  var charsetStack: seq[Charset]
+  for i in countdown(charsets.high, 0):
+    charsetStack.add(charsets[i])
+  var seekable = seekable
+  var inputStream = inputStream
+  if seekable:
+    let scs = inputStream.bomSniff()
+    if scs != CHARSET_UNKNOWN:
+      charsetStack.add(scs)
+      builder.confidence = CONFIDENCE_CERTAIN
+      seekable = false
+  if charsetStack.len == 0:
+    charsetStack.add(DefaultCharset) # UTF-8
+  while true:
+    builder.charset = charsetStack.pop()
+    if seekable:
+      builder.confidence = CONFIDENCE_TENTATIVE # used in the next iteration
+    else:
+      builder.confidence = CONFIDENCE_CERTAIN
+    let em = if charsetStack.len == 0 or not seekable:
+      DECODER_ERROR_MODE_REPLACEMENT
+    else:
+      DECODER_ERROR_MODE_FATAL
+    let decoder = newDecoderStream(inputStream, builder.charset, errormode = em)
+    let encoder = newEncoderStream(decoder, CHARSET_UTF_8,
+      errormode = ENCODER_ERROR_MODE_FATAL)
+    var parser = initHTML5Parser(builder, opts)
+    let document = builder.document
+    var buffer: array[4096, char]
+    while true:
+      let n = encoder.readData(addr buffer[0], buffer.len)
+      if n == 0: break
+      var res = parser.parseChunk(buffer.toOpenArray(0, n - 1))
+      # set insertion point for when it's needed
+      var ip = parser.getInsertionPoint()
+      while res == PRES_SCRIPT:
+        if builder.poppedScript != nil:
+          #TODO microtask
+          document.writeBuffers.add(DocumentWriteBuffer())
+          builder.poppedScript.prepare()
+        while document.parserBlockingScript != nil:
+          let script = document.parserBlockingScript
+          document.parserBlockingScript = nil
+          #TODO style sheet
+          script.execute()
+          assert document.parserBlockingScript != script
+        builder.poppedScript = nil
+        if document.writeBuffers.len == 0:
+          if ip == n:
+            # nothing left to re-parse.
+            break
+          # parse rest of input buffer
+          res = parser.parseChunk(buffer.toOpenArray(ip, n - 1))
+          ip += parser.getInsertionPoint() # move insertion point
+        else:
+          let writeBuffer = document.writeBuffers[^1]
+          let p = writeBuffer.i
+          let n = writeBuffer.data.len
+          res = parser.parseChunk(writeBuffer.data.toOpenArray(p, n - 1))
+          case res
+          of PRES_CONTINUE:
+            discard document.writeBuffers.pop()
+            res = PRES_SCRIPT
+          of PRES_SCRIPT:
+            let pp = p + parser.getInsertionPoint()
+            if pp == writeBuffer.data.len:
+              discard document.writeBuffers.pop()
+            else:
+              writeBuffer.i = pp
+          of PRES_STOP:
+            break
+            {.linearScanEnd.}
+      # PRES_STOP is returned when we return SET_ENCODING_STOP from
+      # setEncodingImpl. We immediately stop parsing in this case.
+      if res == PRES_STOP:
+        break
+    parser.finish()
+    if builder.confidence == CONFIDENCE_CERTAIN and seekable:
+      # A meta tag describing the charset has been found; force use of this
+      # charset.
+      builder.restart()
+      inputStream.setPosition(0)
+      charsetStack.add(builder.charset)
+      seekable = false
+      continue
+    if decoder.failed and seekable:
+      # Retry with another charset.
+      builder.restart()
+      inputStream.setPosition(0)
+      continue
+    break
+  builder.finish()
+  return builder.document
 
 proc newDOMParser(): DOMParser {.jsctor.} =
   return DOMParser()
@@ -265,7 +376,9 @@ proc parseFromString(ctx: JSContext, parser: DOMParser, str, t: string):
       window.document.url
     else:
       newURL("about:blank").get
-    let res = parseHTML(newStringStream(str), Window(nil), url)
+    #TODO this is probably broken in client (or at least sub-optimal)
+    let factory = if window != nil: window.factory else: newCAtomFactory()
+    let res = parseHTML(newStringStream(str), Window(nil), url, factory)
     return ok(res)
   of "text/xml", "application/xml", "application/xhtml+xml", "image/svg+xml":
     return err(newInternalError("XML parsing is not supported yet"))
diff --git a/src/html/dom.nim b/src/html/dom.nim
index ac709ba1..114634e6 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -1,5 +1,4 @@
 import std/deques
-import std/macros
 import std/math
 import std/options
 import std/sets
@@ -11,6 +10,7 @@ import css/cssparser
 import css/sheet
 import css/values
 import display/winattrs
+import html/catom
 import html/enums
 import html/event
 import html/script
@@ -76,6 +76,7 @@ type
     timeouts*: TimeoutState
     navigate*: proc(url: URL)
     importMapsAllowed*: bool
+    factory*: CAtomFactory
 
   # Navigator stuff
   Navigator* = object
@@ -108,13 +109,12 @@ type
   DOMTokenList = ref object
     toks*: seq[string]
     element: Element
-    localName: string
+    localName: CAtom
 
   DOMStringMap = object
     target {.cursor.}: HTMLElement
 
   Node* = ref object of EventTarget
-    nodeType*: NodeType
     childList*: seq[Node]
     parentNode* {.jsget.}: Node
     root: Node
@@ -129,16 +129,18 @@ type
     document_internal: Document # not nil
 
   Attr* = ref object of Node
-    namespaceURI* {.jsget.}: string
-    prefix* {.jsget.}: string
-    localName* {.jsget.}: string
-    value* {.jsget.}: string
-    ownerElement* {.jsget.}: Element
+    dataIdx: int
+    ownerElement*: Element
 
   DOMImplementation = object
     document: Document
 
+  DocumentWriteBuffer* = ref object
+    data*: string
+    i*: int
+
   Document* = ref object of Node
+    factory*: CAtomFactory
     charset*: Charset
     window* {.jsget: "defaultView".}: Window
     url* {.jsget: "URL".}: URL
@@ -148,6 +150,11 @@ type
     implementation {.jsget.}: DOMImplementation
     origin: Origin
     readyState* {.jsget.}: DocumentReadyState
+    # document.write
+    ignoreDestructiveWrites: int
+    throwOnDynamicMarkupInsertion: int
+    activeParserWasAborted: bool
+    writeBuffers*: seq[DocumentWriteBuffer]
 
     scriptsToExecSoon*: seq[HTMLScriptElement]
     scriptsToExecInOrder*: Deque[HTMLScriptElement]
@@ -190,22 +197,30 @@ type
     publicId*: string
     systemId*: string
 
+  AttrData* = object
+    qualifiedName*: CAtom
+    localName*: CAtom
+    prefix*: CAtom
+    namespace*: CAtom
+    value*: string
+
   Element* = ref object of Node
     namespace*: Namespace
-    namespacePrefix*: Option[string]
+    namespacePrefix*: NamespacePrefix
     prefix*: string
-    localName*: string
-    tagType*: TagType
+    localName*: CAtom
 
     id* {.jsget.}: string
     classList* {.jsget.}: DOMTokenList
-    attrs: Table[string, string]
-    attributes* {.jsget.}: NamedNodeMap
+    attrs: seq[AttrData]
+    attributesInternal: NamedNodeMap
     hover*: bool
     invalid*: bool
     style_cached*: CSSStyleDeclaration
     children_cached: HTMLCollection
 
+  AttrDummyElement = ref object of Element
+
   CSSStyleDeclaration* = ref object
     decls*: seq[CSSDeclaration]
     element: Element
@@ -277,7 +292,7 @@ type
     parserDocument*: Document
     preparationTimeDocument*: Document
     forceAsync*: bool
-    fromAnExternalFile*: bool
+    external*: bool
     readyForParserExec*: bool
     alreadyStarted*: bool
     delayingTheLoadEvent: bool
@@ -782,13 +797,64 @@ const ReflectTable0 = [
 ]
 
 # Forward declarations
-func attr*(element: Element, s: string): string {.inline.}
-func attrb*(element: Element, s: string): bool
+func attr*(element: Element, s: CAtom): string
+func attr*(element: Element, s: string): string
+func attrb*(element: Element, s: CAtom): bool
+proc attr*(element: Element, name: CAtom, value: string)
 proc attr*(element: Element, name, value: string)
 func baseURL*(document: Document): URL
+proc delAttr(element: Element, i: int, keep = false)
+proc reflectAttrs(element: Element, name: CAtom, value: string)
+
+func document*(node: Node): Document =
+  if node of Document:
+    return Document(node)
+  return node.document_internal
+
+proc toAtom*(document: Document, s: string): CAtom =
+  return document.factory.toAtom(s)
+
+proc toStr(document: Document, atom: CAtom): string =
+  return document.factory.toStr(atom)
+
+proc toTagType*(document: Document, atom: CAtom): TagType =
+  return document.factory.toTagType(atom)
+
+proc toAtom*(document: Document, tagType: TagType): CAtom =
+  return document.factory.toAtom(tagType)
+
+proc toAtom(document: Document, namespace: Namespace): CAtom =
+  #TODO optimize
+  assert namespace != NO_NAMESPACE
+  return document.toAtom($namespace)
+
+proc toAtom(document: Document, prefix: NamespacePrefix): CAtom =
+  #TODO optimize
+  assert prefix != NO_PREFIX
+  return document.toAtom($prefix)
+
+func tagTypeNoNS(element: Element): TagType =
+  return element.document.toTagType(element.localName)
+
+func tagType*(element: Element): TagType =
+  if element.namespace != Namespace.HTML:
+    return TAG_UNKNOWN
+  return element.tagTypeNoNS
+
+func localNameStr*(element: Element): string =
+  return element.document.toStr(element.localName)
+
+func findAttr(element: Element, qualifiedName: CAtom): int =
+  for i, attr in element.attrs:
+    if attr.qualifiedName == qualifiedName:
+      return i
+  return -1
 
-proc tostr(ftype: enum): string =
-  return ($ftype).split('_')[1..^1].join('-').toLowerAscii()
+func findAttrNS(element: Element, namespace, qualifiedName: CAtom): int =
+  for i, attr in element.attrs:
+    if attr.namespace == namespace and attr.qualifiedName == qualifiedName:
+      return i
+  return -1
 
 func escapeText(s: string, attribute_mode = false): string =
   var nbsp_mode = false
@@ -816,33 +882,30 @@ func escapeText(s: string, attribute_mode = false): string =
 
 func `$`*(node: Node): string =
   if node == nil: return "null" #TODO this isn't standard compliant but helps debugging
-  case node.nodeType
-  of ELEMENT_NODE:
+  if node of Element:
     let element = Element(node)
-    result = "<" & $element.tagType.tostr()
-    for k, v in element.attrs:
-      result &= ' ' & k & "=\"" & v.escapeText(true) & "\""
+    result = "<" & element.localNameStr
+    for attr in element.attrs:
+      let k = element.document.toStr(attr.localName)
+      result &= ' ' & k & "=\"" & attr.value.escapeText(true) & "\""
     result &= ">\n"
     for node in element.childList:
       for line in ($node).split('\n'):
         result &= "\t" & line & "\n"
-    result &= "</" & $element.tagType.tostr() & ">"
-  of TEXT_NODE:
+    result &= "</" & element.localNameStr & ">"
+  elif node of Text:
     let text = Text(node)
     result = text.data.escapeText()
-  of COMMENT_NODE:
+  elif node of Comment:
     result = "<!-- " & Comment(node).data & "-->"
-  of PROCESSING_INSTRUCTION_NODE:
+  elif node of ProcessingInstruction:
     result = "" #TODO
-  of DOCUMENT_TYPE_NODE:
+  elif node of DocumentType:
     result = "<!DOCTYPE" & ' ' & DocumentType(node).name & ">"
+  elif node of Document:
+    result = "Node of Document"
   else:
-    result = "Node of " & $node.nodeType
-
-func document*(node: Node): Document =
-  if node of Document:
-    return Document(node)
-  return node.document_internal
+    result = "Unknown node"
 
 func parentElement*(node: Node): Element {.jsfget.} =
   let p = node.parentNode
@@ -852,13 +915,13 @@ func parentElement*(node: Node): Element {.jsfget.} =
 
 iterator elementList*(node: Node): Element {.inline.} =
   for child in node.childList:
-    if child.nodeType == ELEMENT_NODE:
+    if child of Element:
       yield Element(child)
 
 iterator elementList_rev*(node: Node): Element {.inline.} =
   for i in countdown(node.childList.high, 0):
     let child = node.childList[i]
-    if child.nodeType == ELEMENT_NODE:
+    if child of Element:
       yield Element(child)
 
 # Returns the node's ancestors
@@ -888,7 +951,7 @@ iterator descendants*(node: Node): Node {.inline.} =
 
 iterator elements*(node: Node): Element {.inline.} =
   for child in node.descendants:
-    if child.nodeType == ELEMENT_NODE:
+    if child of Element:
       yield Element(child)
 
 iterator elements*(node: Node, tag: TagType): Element {.inline.} =
@@ -927,7 +990,7 @@ iterator radiogroup*(input: HTMLInputElement): HTMLInputElement {.inline.} =
 
 iterator textNodes*(node: Node): Text {.inline.} =
   for node in node.childList:
-    if node.nodeType == TEXT_NODE:
+    if node of Text:
       yield Text(node)
 
 iterator options*(select: HTMLSelectElement): HTMLOptionElement {.inline.} =
@@ -980,7 +1043,7 @@ proc finalize(collection: HTMLAllCollection) {.jsfin.} =
   collection.finalize0()
 
 func ownerDocument(node: Node): Document {.jsfget.} =
-  if node.nodeType == DOCUMENT_NODE:
+  if node of Document:
     return nil
   return node.document
 
@@ -1005,11 +1068,34 @@ func newCollection[T: Collection](root: Node, match: CollectionMatchFun,
   inc root.document.colln
   result.populateCollection()
 
-func nodeType(node: Node): uint16 {.jsfget.} =
-  return uint16(node.nodeType)
+func jsNodeType0(node: Node): NodeType =
+  if node of CharacterData:
+    if node of Text:
+      return TEXT_NODE
+    elif node of Comment:
+      return COMMENT_NODE
+    elif node of CDATASection:
+      return CDATA_SECTION_NODE
+    elif node of ProcessingInstruction:
+      return PROCESSING_INSTRUCTION_NODE
+    assert false
+  elif node of Element:
+    return ELEMENT_NODE
+  elif node of Document:
+    return DOCUMENT_NODE
+  elif node of DocumentType:
+    return DOCUMENT_TYPE_NODE
+  elif node of Attr:
+    return ATTRIBUTE_NODE
+  elif node of DocumentFragment:
+    return DOCUMENT_FRAGMENT_NODE
+  assert false
+
+func jsNodeType(node: Node): uint16 {.jsfget: "nodeType".} =
+  return uint16(node.jsNodeType0)
 
 func isElement(node: Node): bool =
-  return node.nodeType == ELEMENT_NODE
+  return node of Element
 
 template parentNodeChildrenImpl(parentNode: typed) =
   if parentNode.children_cached == nil:
@@ -1052,7 +1138,8 @@ func contains*(tokenList: DOMTokenList, s: string): bool {.jsfunc.} =
   return s in tokenList.toks
 
 proc update(tokenList: DOMTokenList) =
-  if not tokenList.element.attrb(tokenList.localName) and tokenList.toks.len == 0:
+  if not tokenList.element.attrb(tokenList.localName) and
+      tokenList.toks.len == 0:
     return
   tokenList.element.attr(tokenList.localName, tokenList.toks.join(' '))
 
@@ -1111,18 +1198,22 @@ proc replace(tokenList: DOMTokenList, token, newToken: string):
   return ok(true)
 
 const SupportedTokensMap = {
-  "rel": @["alternate", "dns-prefetch", "icon", "manifest", "modulepreload",
+  "rel": @[
+    "alternate", "dns-prefetch", "icon", "manifest", "modulepreload",
     "next", "pingback", "preconnect", "prefetch", "preload", "search",
-    "stylesheet"]
+    "stylesheet"
+  ]
 }.toTable()
 
 func supports(tokenList: DOMTokenList, token: string):
     JSResult[bool] {.jsfunc.} =
-  if tokenList.localName in SupportedTokensMap:
+  #TODO atomize SupportedTokensMap (or preferably add an attribute name enum)
+  let localName = tokenList.element.document.toStr(tokenList.localName)
+  if localName in SupportedTokensMap:
     let lowercase = token.toLowerAscii()
-    return ok(lowercase in SupportedTokensMap[tokenList.localName])
+    return ok(lowercase in SupportedTokensMap[localName])
   return err(newTypeError("No supported tokens defined for attribute " &
-    tokenList.localName))
+    localName))
 
 func `$`(tokenList: DOMTokenList): string {.jsfunc.} =
   return tokenList.toks.join(' ')
@@ -1134,31 +1225,35 @@ func getter(tokenList: DOMTokenList, i: int): Option[string] {.jsgetprop.} =
   return tokenList.item(i)
 
 # DOMStringMap
-func validateAttributeName(name: string, isq: static bool = false):
-    Err[DOMException] =
-  when isq:
-    if name.matchNameProduction():
-      return ok()
-  else:
-    if name.matchQNameProduction():
-      return ok()
+func validateAttributeName(name: string): Err[DOMException] =
+  if name.matchNameProduction():
+    return ok()
+  return errDOMException("Invalid character in attribute name",
+    "InvalidCharacterError")
+
+func validateAttributeQName(name: string): Err[DOMException] =
+  if name.matchQNameProduction():
+    return ok()
   return errDOMException("Invalid character in attribute name",
     "InvalidCharacterError")
 
 func hasprop(map: ptr DOMStringMap, name: string): bool {.jshasprop.} =
-  return "data-" & name in map[].target.attrs
+  let name = map[].target.document.toAtom("data-" & name)
+  return map[].target.attrb(name)
 
 proc delete(map: ptr DOMStringMap, name: string): bool {.jsfunc.} =
-  let name = "data-" & name.camelToKebabCase()
-  let res = name in map[].target.attrs
-  map[].target.attrs.del(name)
-  return res
+  let name = map[].target.document.toAtom("data-" & name.camelToKebabCase())
+  let i = map[].target.findAttr(name)
+  if i != -1:
+    map[].target.delAttr(i)
+  return i != -1
 
 func getter(map: ptr DOMStringMap, name: string): Option[string]
     {.jsgetprop.} =
-  let name = "data-" & name.camelToKebabCase()
-  map[].target.attrs.withValue(name, p):
-    return some(p[])
+  let name = map[].target.document.toAtom("data-" & name.camelToKebabCase())
+  let i = map[].target.findAttr(name)
+  if i != -1:
+    return some(map[].target.attrs[i].value)
   return none(string)
 
 proc setter(map: ptr DOMStringMap, name, value: string): Err[DOMException]
@@ -1172,13 +1267,15 @@ proc setter(map: ptr DOMStringMap, name, value: string): Err[DOMException]
       "InvalidCharacterError")
   let name = "data-" & name.camelToKebabCase()
   ?name.validateAttributeName()
-  map.target.attr(name, value)
+  let aname = map[].target.document.toAtom(name)
+  map.target.attr(aname, value)
   return ok()
 
 func names(ctx: JSContext, map: ptr DOMStringMap): JSPropertyEnumList
     {.jspropnames.} =
   var list = newJSPropertyEnumList(ctx, uint32(map[].target.attrs.len))
-  for k, v in map[].target.attrs:
+  for attr in map[].target.attrs:
+    let k = map[].target.document.toStr(attr.localName)
     if k.startsWith("data-") and AsciiUpperAlpha notin k:
       list.add(k["data-".len .. ^1].kebabToCamelCase())
   return list
@@ -1231,6 +1328,7 @@ func getter[T: uint32|string](collection: HTMLCollection, u: T):
 
 func names(ctx: JSContext, collection: HTMLCollection): JSPropertyEnumList
     {.jspropnames.} =
+  let aName = collection.root.document.toAtom("name") #TODO enumize
   let L = collection.length
   var list = newJSPropertyEnumList(ctx, L)
   var ids: OrderedSet[string]
@@ -1240,7 +1338,7 @@ func names(ctx: JSContext, collection: HTMLCollection): JSPropertyEnumList
     if elem.id != "":
       ids.incl(elem.id)
     if elem.namespace == Namespace.HTML:
-      let name = elem.attr("name")
+      let name = elem.attr(aName)
       ids.incl(name)
   for id in ids:
     list.add(id)
@@ -1423,100 +1521,127 @@ proc setHash(location: Location, s: string) {.jsfset: "hash".} =
   copyURL.setHash(s)
   document.window.navigate(copyURL)
 
-func newAttr(document: Document, localName, value, prefix,
-    namespaceURI: string): Attr =
-  return Attr(
-    nodeType: ATTRIBUTE_NODE,
-    document_internal: document,
-    namespaceURI: namespaceURI,
-    localName: localName,
-    prefix: prefix,
-    value: value,
-    index: -1
-  )
+func jsOwnerElement(attr: Attr): Element {.jsfget: "ownerElement".} =
+  if attr.ownerElement of AttrDummyElement:
+    return nil
+  return attr.ownerElement
 
-func newAttr(parent: Element, localName, value: string, prefix = "",
-    namespaceURI = ""): Attr =
-  return Attr(
-    nodeType: ATTRIBUTE_NODE,
-    document_internal: parent.document,
-    namespaceURI: namespaceURI,
-    ownerElement: parent,
-    localName: localName,
-    prefix: prefix,
-    value: value,
-    index: -1
-  )
+func data(attr: Attr): lent AttrData =
+  return attr.ownerElement.attrs[attr.dataIdx]
+
+proc jsNamespaceURI(attr: Attr): string {.jsfget: "namespaceURI".} =
+  return attr.ownerElement.document.toStr(attr.data.namespace)
+
+proc jsPrefix(attr: Attr): string {.jsfget: "prefix".} =
+  return attr.ownerElement.document.toStr(attr.data.prefix)
+
+proc jsLocalName(attr: Attr): string {.jsfget: "localName".} =
+  return attr.ownerElement.document.toStr(attr.data.localName)
+
+proc jsValue(attr: Attr): string {.jsfget: "value".} =
+  return attr.data.value
 
 func name(attr: Attr): string {.jsfget.} =
-  if attr.prefix == "":
-    return attr.localName
-  return attr.prefix & ':' & attr.localName
+  return attr.ownerElement.document.toStr(attr.data.qualifiedName)
 
-func findAttr(map: NamedNodeMap, name: string): int =
-  for i in 0 ..< map.attrlist.len:
-    if map.attrlist[i].name == name:
+func findAttr(map: NamedNodeMap, dataIdx: int): int =
+  for i, attr in map.attrlist:
+    if attr.dataIdx == dataIdx:
       return i
   return -1
 
-func findAttrNS(map: NamedNodeMap, namespace, localName: string): int =
-  for i in 0 ..< map.attrlist.len:
-    if map.attrlist[i].namespaceURI == namespace and map.attrlist[i].localName == localName:
-      return i
-  return -1
+proc getAttr(map: NamedNodeMap, dataIdx: int): Attr =
+  let i = map.findAttr(dataIdx)
+  if i != -1:
+    return map.attrlist[i]
+  let attr = Attr(
+    document_internal: map.element.document,
+    index: -1,
+    dataIdx: dataIdx,
+    ownerElement: map.element
+  )
+  map.attrlist.add(attr)
+  return attr
+
+func normalizeAttrQName(element: Element, qualifiedName: string): CAtom =
+  if element.namespace == Namespace.HTML and not element.document.isxml:
+    return element.document.toAtom(qualifiedName.toLowerAscii())
+  return element.document.toAtom(qualifiedName)
+
+func hasAttributes(element: Element): bool {.jsfunc.} =
+  return element.attrs.len > 0
+
+func attributes(element: Element): NamedNodeMap {.jsfget.} =
+  if element.attributesInternal != nil:
+    return element.attributesInternal
+  element.attributesInternal = NamedNodeMap(element: element)
+  for i, attr in element.attrs:
+    element.attributesInternal.attrlist.add(Attr(
+      document_internal: element.document,
+      index: -1,
+      dataIdx: i,
+      ownerElement: element
+    ))
+  return element.attributesInternal
+
+func findAttr(element: Element, qualifiedName: string): int =
+  return element.findAttr(element.normalizeAttrQName(qualifiedName))
+
+func findAttrNS(element: Element, namespace, localName: string): int =
+  let namespace = element.document.toAtom(namespace)
+  let localName = element.document.toAtom(localName)
+  return element.findAttrNS(namespace, localName)
 
 func hasAttribute(element: Element, qualifiedName: string): bool {.jsfunc.} =
-  let qualifiedName = if element.namespace == Namespace.HTML and
-      not element.document.isxml:
-    qualifiedName.toLowerAscii()
-  else:
-    qualifiedName
-  if qualifiedName in element.attrs:
-    return true
+  return element.findAttr(qualifiedName) != -1
 
 func hasAttributeNS(element: Element, namespace, localName: string): bool {.jsfunc.} =
-  return element.attributes.findAttrNS(namespace, localName) != -1
+  return element.findAttrNS(namespace, localName) != -1
 
 func getAttribute(element: Element, qualifiedName: string): Option[string] {.jsfunc.} =
-  let qualifiedName = if element.namespace == Namespace.HTML and
-      not element.document.isxml:
-    qualifiedName.toLowerAscii()
-  else:
-    qualifiedName
-  element.attrs.withValue(qualifiedName, val):
-    return some(val[])
+  let i = element.findAttr(qualifiedName)
+  if i != -1:
+    return some(element.attrs[i].value)
+  return none(string)
 
-func getAttributeNS(element: Element, namespace, localName: string): Option[string] {.jsfunc.} =
-  let i = element.attributes.findAttrNS(namespace, localName)
+func getAttributeNS(element: Element, namespace, localName: string):
+    Option[string] {.jsfunc.} =
+  let i = element.findAttrNS(namespace, localName)
   if i != -1:
-    return some(element.attributes.attrlist[i].value)
+    return some(element.attrs[i].value)
+  return none(string)
 
-func getNamedItem(map: NamedNodeMap, qualifiedName: string): Option[Attr] {.jsfunc.} =
-  if map.element.hasAttribute(qualifiedName):
-    let i = map.findAttr(qualifiedName)
-    if i != -1:
-      return some(map.attrlist[i])
+proc getNamedItem(map: NamedNodeMap, qualifiedName: string): Option[Attr]
+    {.jsfunc.} =
+  let i = map.element.findAttr(qualifiedName)
+  if i != -1:
+    return some(map.getAttr(i))
+  return none(Attr)
 
-func getNamedItemNS(map: NamedNodeMap, namespace, localName: string): Option[Attr] {.jsfunc.} =
-  let i = map.findAttrNS(namespace, localName)
+proc getNamedItemNS(map: NamedNodeMap, namespace, localName: string):
+    Option[Attr] {.jsfunc.} =
+  let i = map.element.findAttrNS(namespace, localName)
   if i != -1:
-    return some(map.attrlist[i])
+    return some(map.getAttr(i))
+  return none(Attr)
 
 func length(map: NamedNodeMap): uint32 {.jsfget.} =
   return uint32(map.element.attrs.len)
 
-func item(map: NamedNodeMap, i: int): Option[Attr] {.jsfunc.} =
-  if i < map.attrlist.len:
-    return some(map.attrlist[i])
+proc item(map: NamedNodeMap, i: uint32): Option[Attr] {.jsfunc.} =
+  if int(i) < map.element.attrs.len:
+    return some(map.getAttr(int(i)))
+  return none(Attr)
 
-func hasprop[T: int|string](map: NamedNodeMap, i: T): bool {.jshasprop.} =
-  when T is int:
-    return i < map.attrlist.len
+func hasprop[T: uint32|string](map: NamedNodeMap, i: T): bool {.jshasprop.} =
+  when T is uint32:
+    return int(i) < map.element.attrs.len
   else:
     return map.getNamedItem(i).isSome
 
-func getter[T: int|string](map: NamedNodeMap, i: T): Option[Attr] {.jsgetprop.} =
-  when T is int:
+func getter[T: uint32|string](map: NamedNodeMap, i: T): Option[Attr]
+    {.jsgetprop.} =
+  when T is uint32:
     return map.item(i)
   else:
     return map.getNamedItem(i)
@@ -1530,9 +1655,16 @@ func names(ctx: JSContext, map: NamedNodeMap): JSPropertyEnumList
   var list = newJSPropertyEnumList(ctx, len)
   for u in 0 ..< len:
     list.add(u)
-  if map.element.namespace == Namespace.HTML:
-    for name in map.element.attrs.keys:
-      list.add(name)
+  var names: HashSet[string]
+  let element = map.element
+  for attr in element.attrs:
+    let name = element.document.toStr(attr.qualifiedName)
+    if element.namespace == Namespace.HTML and AsciiUpperAlpha in name:
+      continue
+    if name in names:
+      continue
+    names.incl(name)
+    list.add(name)
   return list
 
 func length(characterData: CharacterData): uint32 {.jsfget.} =
@@ -1590,10 +1722,28 @@ func canSubmitImplicitly*(form: HTMLFormElement): bool =
   return true
 
 func qualifiedName*(element: Element): string =
-  if element.namespacePrefix.isSome:
-    element.namespacePrefix.get & ':' & element.localName
+  if element.namespacePrefix != NO_PREFIX:
+    $element.namespacePrefix & ':' & element.localNameStr
   else:
-    element.localName
+    element.localNameStr
+
+# https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#document-write-steps
+proc write(document: Document, text: varargs[string]): Err[DOMException]
+    {.jsfunc.} =
+  if document.isxml:
+    return errDOMException("document.write not supported in XML documents",
+      "InvalidStateError")
+  if document.throwOnDynamicMarkupInsertion > 0:
+    return errDOMException("throw-on-dynamic-markup-insertion counter > 0",
+      "InvalidStateError")
+  if document.activeParserWasAborted:
+    return ok()
+  #TODO if insertion point is undefined... (open document)
+  if document.writeBuffers.len == 0:
+    return ok() #TODO (probably covered by open above)
+  for s in text:
+    document.writeBuffers[^1].data &= s
+  return ok()
 
 func html*(document: Document): HTMLElement =
   for element in document.elements(TAG_HTML):
@@ -1617,21 +1767,21 @@ func select*(option: HTMLOptionElement): HTMLSelectElement =
       return HTMLSelectElement(anc)
   return nil
 
-func countChildren(node: Node, nodeType: NodeType): int =
+func countChildren(node: Node, nodeType: type): int =
   for child in node.childList:
-    if child.nodeType == nodeType:
+    if child of nodeType:
       inc result
 
-func hasChild(node: Node, nodeType: NodeType): bool =
+func hasChild(node: Node, nodeType: type): bool =
   for child in node.childList:
-    if child.nodeType == nodeType:
+    if child of nodeType:
       return true
 
-func hasChildExcept(node: Node, nodeType: NodeType, ex: Node): bool =
+func hasChildExcept(node: Node, nodeType: type, ex: Node): bool =
   for child in node.childList:
     if child == ex:
       continue
-    if child.nodeType == nodeType:
+    if child of nodeType:
       return true
   return false
 
@@ -1647,42 +1797,46 @@ func nextSibling*(node: Node): Node {.jsfget.} =
     return nil
   return node.parentNode.childList[i]
 
-func hasNextSibling(node: Node, nodeType: NodeType): bool =
+func hasNextSibling(node: Node, nodeType: type): bool =
   var node = node.nextSibling
   while node != nil:
-    if node.nodeType == nodeType: return true
+    if node of nodeType:
+      return true
     node = node.nextSibling
   return false
 
-func hasPreviousSibling(node: Node, nodeType: NodeType): bool =
+func hasPreviousSibling(node: Node, nodeType: type): bool =
   var node = node.previousSibling
   while node != nil:
-    if node.nodeType == nodeType: return true
+    if node of nodeType:
+      return true
     node = node.previousSibling
   return false
 
 func nodeValue(node: Node): Option[string] {.jsfget.} =
-  case node.nodeType
-  of CharacterDataNodes:
+  if node of CharacterData:
     return some(CharacterData(node).data)
-  of ATTRIBUTE_NODE:
-    return some(Attr(node).value)
-  else: discard
+  elif node of Attr:
+    return some(Attr(node).data.value)
+  return none(string)
 
-func textContent*(node: Node): string {.jsfget.} =
-  case node.nodeType
-  of DOCUMENT_NODE, DOCUMENT_TYPE_NODE:
-    return "" #TODO null
-  of CharacterDataNodes:
-    return CharacterData(node).data
+func textContent*(node: Node): string =
+  if node of CharacterData:
+    result = CharacterData(node).data
   else:
+    result = ""
     for child in node.childList:
-      if child.nodeType != COMMENT_NODE:
+      if not (child of Comment):
         result &= child.textContent
 
+func jsTextContent(node: Node): Opt[string] {.jsfget: "textContent".} =
+  if node of Document or node of DocumentType:
+    return err() # null
+  return ok(node.textContent)
+
 func childTextContent*(node: Node): string =
   for child in node.childList:
-    if child.nodeType == TEXT_NODE:
+    if child of Text:
       result &= Text(child).data
 
 func rootNode*(node: Node): Node =
@@ -1690,7 +1844,7 @@ func rootNode*(node: Node): Node =
   return node.root
 
 func isConnected(node: Node): bool {.jsfget.} =
-  return node.rootNode.nodeType == DOCUMENT_NODE #TODO shadow root
+  return node.rootNode of Document #TODO shadow root
 
 func inSameTree*(a, b: Node): bool =
   a.rootNode == b.rootNode
@@ -1735,13 +1889,30 @@ func getElementById(node: Node, id: string): Element {.jsfunc.} =
   for child in node.elements:
     if child.id == id:
       return child
+  return nil
 
 func getElementsByTagName0(root: Node, tagName: string): HTMLCollection =
   if tagName == "*":
-    return newCollection[HTMLCollection](root, func(node: Node): bool = node.isElement, true, false)
-  let t = tagType(tagName)
-  if t != TAG_UNKNOWN:
-    return newCollection[HTMLCollection](root, func(node: Node): bool = node.isElement and Element(node).tagType == t, true, false)
+    return newCollection[HTMLCollection](
+      root,
+      isElement,
+      islive = true,
+      childonly = false
+    )
+  let localName = root.document.toAtom(tagName)
+  let localNameLower = root.document.toAtom(tagName.toLowerAscii())
+  return newCollection[HTMLCollection](
+    root,
+    func(node: Node): bool =
+      if node of Element:
+        let element = Element(node)
+        if element.namespace == Namespace.HTML:
+          return element.localName == localNameLower
+        return element.localName == localName
+      return false,
+    islive = true,
+    childonly = false
+  )
 
 func getElementsByTagName(document: Document, tagName: string): HTMLCollection {.jsfunc.} =
   return document.getElementsByTagName0(tagName)
@@ -1758,7 +1929,7 @@ func getElementsByClassName0(node: Node, classNames: string): HTMLCollection =
         c = c.toLowerAscii()
   return newCollection[HTMLCollection](node,
     func(node: Node): bool =
-      if node.nodeType == ELEMENT_NODE:
+      if node of Element:
         if isquirks:
           var cl = Element(node).classList
           for tok in cl.toks.mitems:
@@ -1784,7 +1955,7 @@ func previousElementSibling*(elem: Element): Element {.jsfget.} =
   if p == nil: return nil
   for i in countdown(elem.index - 1, 0):
     let node = p.childList[i]
-    if node.nodeType == ELEMENT_NODE:
+    if node of Element:
       return Element(node)
   return nil
 
@@ -1793,15 +1964,21 @@ func nextElementSibling*(elem: Element): Element {.jsfget.} =
   if p == nil: return nil
   for i in elem.index + 1 .. p.childList.high:
     let node = p.childList[i]
-    if node.nodeType == ELEMENT_NODE:
+    if node of Element:
       return Element(node)
   return nil
 
 func documentElement(document: Document): Element {.jsfget.} =
   document.firstElementChild()
 
-func attr*(element: Element, s: string): string {.inline.} =
-  return element.attrs.getOrDefault(s, "")
+func attr*(element: Element, s: CAtom): string =
+  let i = element.findAttr(s)
+  if i != -1:
+    return element.attrs[i].value
+  return ""
+
+func attr*(element: Element, s: string): string =
+  return element.attr(element.document.toAtom(s))
 
 func attrl*(element: Element, s: string): Option[int32] =
   return parseInt32(element.attr(s))
@@ -1816,10 +1993,12 @@ func attrul*(element: Element, s: string): Option[uint32] =
   if x.isSome and x.get >= 0:
     return x
 
+func attrb*(element: Element, s: CAtom): bool =
+  return element.findAttr(s) != -1
+
 func attrb*(element: Element, s: string): bool =
-  if s in element.attrs:
-    return true
-  return false
+  let atom = element.document.toAtom(s)
+  return element.attrb(atom)
 
 # https://html.spec.whatwg.org/multipage/parsing.html#serialising-html-fragments
 func serializesAsVoid(element: Element): bool =
@@ -1832,23 +2011,19 @@ func serializeFragmentInner(child: Node, parentType: TagType): string =
   result = ""
   if child of Element:
     let element = Element(child)
+    let tags = element.localNameStr
     result &= '<'
     #TODO qualified name if not HTML, SVG or MathML
-    if element.tagType == TAG_UNKNOWN:
-      result &= element.localName
-    else:
-      result &= tagName(element.tagType)
+    result &= tags
     #TODO custom elements
-    for k, v in element.attrs:
+    for attr in element.attrs:
       #TODO namespaced attrs
-      result &= ' ' & k & "=\"" & v.escapeText(true) & "\""
+      let k = element.document.toStr(attr.localName)
+      result &= ' ' & k & "=\"" & attr.value.escapeText(true) & "\""
     result &= '>'
     result &= element.serializeFragment()
     result &= "</"
-    if element.tagType == TAG_UNKNOWN:
-      result &= element.localName
-    else:
-      result &= tagName(element.tagType)
+    result &= tags
     result &= '>'
   elif child of Text:
     let text = Text(child)
@@ -1924,7 +2099,7 @@ proc sheets*(document: Document): seq[CSSStylesheet] =
       case elem.tagType
       of TAG_STYLE:
         let style = HTMLStyleElement(elem)
-        style.sheet = parseStylesheet(newStringStream(style.textContent))
+        style.sheet = parseStylesheet(style.textContent, document.factory)
         if style.sheet != nil:
           document.cachedSheets.add(style.sheet)
       of TAG_LINK:
@@ -2162,7 +2337,6 @@ func jsForm(this: HTMLTextAreaElement): HTMLFormElement {.jsfget: "form".} =
 
 func newText(document: Document, data: string): Text =
   return Text(
-    nodeType: TEXT_NODE,
     document_internal: document,
     data: data,
     index: -1
@@ -2174,7 +2348,6 @@ func newText(ctx: JSContext, data = ""): Text {.jsctor.} =
 
 func newCDATASection(document: Document, data: string): CDATASection =
   return CDATASection(
-    nodeType: CDATA_SECTION_NODE,
     document_internal: document,
     data: data,
     index: -1
@@ -2183,7 +2356,6 @@ func newCDATASection(document: Document, data: string): CDATASection =
 func newProcessingInstruction(document: Document, target, data: string):
     ProcessingInstruction =
   return ProcessingInstruction(
-    nodeType: PROCESSING_INSTRUCTION_NODE,
     document_internal: document,
     target: target,
     data: data,
@@ -2192,7 +2364,6 @@ func newProcessingInstruction(document: Document, target, data: string):
 
 func newDocumentFragment(document: Document): DocumentFragment =
   return DocumentFragment(
-    nodeType: DOCUMENT_FRAGMENT_NODE,
     document_internal: document,
     index: -1
   )
@@ -2203,7 +2374,6 @@ func newDocumentFragment(ctx: JSContext): DocumentFragment {.jsctor.} =
 
 func newComment(document: Document, data: string): Comment =
   return Comment(
-    nodeType: COMMENT_NODE,
     document_internal: document,
     data: data,
     index: -1
@@ -2214,15 +2384,17 @@ func newComment(ctx: JSContext, data: string = ""): Comment {.jsctor.} =
   return window.document.newComment(data)
 
 #TODO custom elements
-func newHTMLElement*(document: Document, tagType: TagType,
-    namespace = Namespace.HTML, prefix = none[string](),
-    attrs = Table[string, string]()): HTMLElement =
+proc newHTMLElement*(document: Document, localName: CAtom,
+    namespace = Namespace.HTML, prefix = NO_PREFIX,
+    attrs = newSeq[AttrData]()): HTMLElement =
+  let tagType = document.toTagType(localName)
   case tagType
   of TAG_INPUT:
     result = HTMLInputElement()
   of TAG_A:
     let anchor = HTMLAnchorElement()
-    anchor.relList = DOMTokenList(element: anchor, localName: "rel")
+    let localName = document.toAtom("rel")
+    anchor.relList = DOMTokenList(element: anchor, localName: localName)
     result = anchor
   of TAG_SELECT:
     result = HTMLSelectElement()
@@ -2248,11 +2420,13 @@ func newHTMLElement*(document: Document, tagType: TagType,
     result = HTMLStyleElement()
   of TAG_LINK:
     let link = HTMLLinkElement()
-    link.relList = DOMTokenList(element: link, localName: "rel")
+    let localName = document.toAtom("rel") #TODO enumize
+    link.relList = DOMTokenList(element: link, localName: localName)
     result = link
   of TAG_FORM:
     let form = HTMLFormElement()
-    form.relList = DOMTokenList(element: form, localName: "rel")
+    let localName = document.toAtom("rel") #TODO enumize
+    form.relList = DOMTokenList(element: form, localName: localName)
     result = form
   of TAG_TEMPLATE:
     result = HTMLTemplateElement(
@@ -2279,52 +2453,52 @@ func newHTMLElement*(document: Document, tagType: TagType,
     result = HTMLImageElement()
   of TAG_AREA:
     let area = HTMLAreaElement()
-    area.relList = DOMTokenList(element: result, localName: "rel")
+    let localName = document.toAtom("rel") #TODO enumize
+    area.relList = DOMTokenList(element: result, localName: localName)
     result = area
   else:
     result = HTMLElement()
-  result.nodeType = ELEMENT_NODE
-  result.tagType = tagType
+  result.localName = localName
   result.namespace = namespace
   result.namespacePrefix = prefix
   result.document_internal = document
-  result.attributes = NamedNodeMap(element: result)
-  result.classList = DOMTokenList(element: result, localName: "classList")
+  let localName = document.toAtom("classList") #TODO enumize
+  result.classList = DOMTokenList(element: result, localName: localName)
   result.index = -1
   result.dataset = DOMStringMap(target: result)
-  {.cast(noSideEffect).}:
-    for k, v in attrs:
-      result.attr(k, v)
-  case tagType
-  of TAG_SCRIPT:
-    HTMLScriptElement(result).internalNonce = result.attr("nonce")
-  of TAG_CANVAS:
-    HTMLCanvasElement(result).bitmap = newBitmap(
-      width = result.attrul("width").get(300),
-      height = result.attrul("height").get(150)
-    )
-  else: discard
+  result.attrs = attrs
 
-func newHTMLElement*(document: Document, localName: string,
-    namespace = Namespace.HTML, prefix = none[string](),
-    tagType = tagType(localName), attrs = Table[string, string]()): Element =
-  result = document.newHTMLElement(tagType, namespace, prefix, attrs)
-  if tagType == TAG_UNKNOWN:
-    result.localName = localName
+proc newHTMLElement*(document: Document, tagType: TagType,
+    namespace = Namespace.HTML, prefix = NO_PREFIX,
+    attrs = newSeq[AttrData]()): HTMLElement =
+  let localName = document.toAtom(tagType)
+  return document.newHTMLElement(localName, namespace, prefix, attrs)
 
-func newDocument*(): Document {.jsctor.} =
+func newDocument*(factory: CAtomFactory): Document =
+  assert factory != nil
   let document = Document(
-    nodeType: DOCUMENT_NODE,
     url: newURL("about:blank").get,
-    index: -1
+    index: -1,
+    factory: factory
   )
   document.implementation = DOMImplementation(document: document)
   document.contentType = "application/xml"
   return document
 
-func newDocumentType*(document: Document, name: string, publicId = "", systemId = ""): DocumentType =
+func newDocument(ctx: JSContext): Document {.jsctor.} =
+  let global = JS_GetGlobalObject(ctx)
+  let window = if ctx.hasClass(Window):
+    fromJS[Window](ctx, global).get(nil)
+  else:
+    Window(nil)
+  JS_FreeValue(ctx, global)
+  #TODO this is probably broken in client (or at least sub-optimal)
+  let factory = if window != nil: window.factory else: newCAtomFactory()
+  return newDocument(factory)
+
+func newDocumentType*(document: Document, name, publicId, systemId: string):
+    DocumentType =
   return DocumentType(
-    nodeType: DOCUMENT_TYPE_NODE,
     document_internal: document,
     name: name,
     publicId: publicId,
@@ -2332,15 +2506,13 @@ func newDocumentType*(document: Document, name: string, publicId = "", systemId
     index: -1
   )
 
-func isResettable*(element: Element): bool =
-  return element.tagType in {TAG_INPUT, TAG_OUTPUT, TAG_SELECT, TAG_TEXTAREA}
-
 func isHostIncludingInclusiveAncestor*(a, b: Node): bool =
   for parent in b.branch:
     if parent == a:
       return true
-  if b.rootNode.nodeType == DOCUMENT_FRAGMENT_NODE and DocumentFragment(b.rootNode).host != nil:
-    for parent in b.rootNode.branch:
+  let root = b.rootNode
+  if root of DocumentFragment and DocumentFragment(root).host != nil:
+    for parent in root.branch:
       if parent == a:
         return true
   return false
@@ -2375,39 +2547,55 @@ func title*(document: Document): string =
     return title.childTextContent.stripAndCollapse()
   return ""
 
-func disabled*(option: HTMLOptionElement): bool =
-  if option.parentElement.tagType == TAG_OPTGROUP and option.parentElement.attrb("disabled"):
+# https://html.spec.whatwg.org/multipage/form-elements.html#concept-option-disabled
+func isDisabled*(option: HTMLOptionElement): bool =
+  if option.parentElement.tagType == TAG_OPTGROUP and
+      option.parentElement.attrb("disabled"):
     return true
   return option.attrb("disabled")
 
-func text*(option: HTMLOptionElement): string =
+func text(option: HTMLOptionElement): string {.jsfget.} =
+  var s = ""
   for child in option.descendants:
-    if child.nodeType == TEXT_NODE:
-      let child = Text(child)
-      if child.parentElement.tagType != TAG_SCRIPT: #TODO svg
-        result &= child.data.stripAndCollapse()
+    let parent = child.parentElement
+    if child of Text and (parent.tagTypeNoNS != TAG_SCRIPT or
+        parent.namespace notin {Namespace.HTML, Namespace.SVG}):
+      s &= Text(child).data
+  return s.stripAndCollapse()
 
 func value*(option: HTMLOptionElement): string {.jsfget.} =
   if option.attrb("value"):
     return option.attr("value")
-  return option.childTextContent.stripAndCollapse()
+  return option.text
 
 proc invalidateCollections(node: Node) =
   for id in node.liveCollections:
     node.document.invalidCollections.incl(id)
 
-proc delAttr(element: Element, i: int) =
-  if i != -1:
-    let attr = element.attributes.attrlist[i]
-    element.attrs.del(attr.name)
-    element.attributes.attrlist.delete(i)
-    element.invalidateCollections()
-    element.invalid = true
-
-proc delAttr(element: Element, name: string) =
-  let i = element.attributes.findAttr(name)
-  if i != -1:
-    element.delAttr(i)
+proc delAttr(element: Element, i: int, keep = false) =
+  let map = element.attributesInternal
+  element.attrs.delete(i) # ordering matters
+  if map != nil:
+    # delete from attrlist + adjust indices invalidated
+    var j = -1
+    for i, attr in map.attrlist.mpairs:
+      if attr.dataIdx == i:
+        j = i
+      elif attr.dataIdx > i:
+        dec attr.dataIdx
+    if j != -1:
+      if keep:
+        let attr = map.attrlist[j]
+        let data = attr.data
+        attr.ownerElement = AttrDummyElement(
+          document_internal: attr.ownerElement.document,
+          index: -1,
+          attrs: @[data]
+        )
+        attr.dataIdx = 0
+      map.attrlist.del(j) # ordering does not matter
+  element.invalidateCollections()
+  element.invalid = true
 
 proc newCSSStyleDeclaration(element: Element, value: string):
     CSSStyleDeclaration =
@@ -2463,7 +2651,9 @@ proc style*(element: Element): CSSStyleDeclaration {.jsfget.} =
     element.style_cached = CSSStyleDeclaration(element: element)
   return element.style_cached
 
-proc reflectAttrs(element: Element, name, value: string) =
+proc reflectAttrs(element: Element, name: CAtom, value: string) =
+  #TODO enumize
+  let name = element.document.toStr(name)
   template reflect_str(element: Element, n: static string, val: untyped) =
     if name == n:
       element.val = value
@@ -2486,6 +2676,7 @@ proc reflectAttrs(element: Element, name, value: string) =
       return
   element.reflect_str "id", id
   element.reflect_domtoklist "class", classList
+  #TODO internalNonce
   if name == "style":
     element.style_cached = newCSSStyleDeclaration(element, value)
     return
@@ -2514,24 +2705,64 @@ proc reflectAttrs(element: Element, name, value: string) =
   of TAG_AREA:
     let area = HTMLAreaElement(element)
     area.reflect_domtoklist "rel", relList
+  of TAG_CANVAS:
+    if name == "width" or name == "height":
+      let w = element.attrul("width").get(300)
+      let h = element.attrul("height").get(150)
+      let canvas = HTMLCanvasElement(element)
+      if canvas.bitmap.width != w or canvas.bitmap.height != h:
+        canvas.bitmap = newBitmap(w, h)
   else: discard
 
-proc attr0(element: Element, name, value: string) =
-  element.attrs.withValue(name, val):
-    val[] = value
+proc attr*(element: Element, name: CAtom, value: string) =
+  let i = element.findAttr(name)
+  if i != -1:
+    element.attrs[i].value = value
     element.invalidateCollections()
     element.invalid = true
-  do: # else
-    element.attrs[name] = value
+  else:
+    #TODO sort?
+    element.attrs.add(AttrData(
+      qualifiedName: name,
+      localName: name,
+      value: value
+    ))
   element.reflectAttrs(name, value)
 
-proc attr*(element: Element, name, value: string) =
-  let i = element.attributes.findAttr(name)
+proc attrns*(element: Element, localName: CAtom, prefix: NamespacePrefix,
+    namespace: Namespace, value: sink string) =
+  if prefix == NO_PREFIX and namespace == NO_NAMESPACE:
+    element.attr(localName, value)
+    return
+  let namespace = element.document.toAtom(namespace)
+  let i = element.findAttrNS(namespace, localName)
+  var prefixAtom, qualifiedName: CAtom
+  if prefix != NO_PREFIX:
+    prefixAtom = element.document.toAtom(prefix)
+    let tmp = $prefix & ':' & element.document.toStr(localName)
+    qualifiedName = element.document.toAtom(tmp)
+  else:
+    qualifiedName = localName
   if i != -1:
-    element.attributes.attrlist[i].value = value
+    element.attrs[i].prefix = prefixAtom
+    element.attrs[i].qualifiedName = qualifiedName
+    element.attrs[i].value = value
+    element.invalidateCollections()
+    element.invalid = true
   else:
-    element.attributes.attrlist.add(element.newAttr(name, value))
-  element.attr0(name, value)
+    #TODO sort?
+    element.attrs.add(AttrData(
+      prefix: prefixAtom,
+      localName: localName,
+      qualifiedName: qualifiedName,
+      namespace: namespace,
+      value: value
+    ))
+  element.reflectAttrs(qualifiedName, value)
+
+proc attr*(element: Element, name, value: string) =
+  let name = element.document.toAtom(name)
+  element.attr(name, value)
 
 proc attrl(element: Element, name: string, value: int32) =
   element.attr(name, $value)
@@ -2555,72 +2786,76 @@ proc setAttribute(element: Element, qualifiedName, value: string):
 
 proc setAttributeNS(element: Element, namespace, qualifiedName,
     value: string): Err[DOMException] {.jsfunc.} =
-  ?validateAttributeName(qualifiedName, isq = true)
+  ?validateAttributeQName(qualifiedName)
   let ps = qualifiedName.until(':')
   let prefix = if ps.len < qualifiedName.len: ps else: ""
-  let localName = qualifiedName.substr(prefix.len)
+  let localName = element.document.toAtom(qualifiedName.substr(prefix.len))
+  #TODO atomize here
   if prefix != "" and namespace == "" or
       prefix == "xml" and namespace != $Namespace.XML or
       (qualifiedName == "xmlns" or prefix == "xmlns") and namespace != $Namespace.XMLNS or
       namespace == $Namespace.XMLNS and qualifiedName != "xmlns" and prefix != "xmlns":
     return errDOMException("Unexpected namespace", "NamespaceError")
-  element.attr0(qualifiedName, value)
-  let i = element.attributes.findAttrNS(namespace, localName)
+  let qualifiedName = element.document.toAtom(qualifiedName)
+  let namespace = element.document.toAtom(namespace)
+  let i = element.findAttrNS(namespace, localName)
   if i != -1:
-    element.attributes.attrlist[i].value = value
+    element.attrs[i].value = value
   else:
-    element.attributes.attrlist.add(element.newAttr(localName, value, prefix, namespace))
+    element.attrs.add(AttrData(
+      localName: localName,
+      namespace: namespace,
+      qualifiedName: qualifiedName,
+      value: value
+    ))
   return ok()
 
 proc removeAttribute(element: Element, qualifiedName: string) {.jsfunc.} =
-  let qualifiedName = if element.namespace == Namespace.HTML and not element.document.isxml:
-    qualifiedName.toLowerAscii()
-  else:
-    qualifiedName
-  element.delAttr(qualifiedName)
+  let i = element.findAttr(qualifiedName)
+  if i != -1:
+    element.delAttr(i)
 
 proc removeAttributeNS(element: Element, namespace, localName: string) {.jsfunc.} =
-  let i = element.attributes.findAttrNS(namespace, localName)
+  let i = element.findAttrNS(namespace, localName)
   if i != -1:
     element.delAttr(i)
 
 proc toggleAttribute(element: Element, qualifiedName: string,
     force = none(bool)): DOMResult[bool] {.jsfunc.} =
   ?validateAttributeName(qualifiedName)
-  let qualifiedName = if element.namespace == Namespace.HTML and not element.document.isxml:
-    qualifiedName.toLowerAscii()
-  else:
-    qualifiedName
+  let qualifiedName = element.normalizeAttrQName(qualifiedName)
   if not element.attrb(qualifiedName):
     if force.get(true):
       element.attr(qualifiedName, "")
       return ok(true)
     return ok(false)
   if not force.get(false):
-    element.delAttr(qualifiedName)
+    let i = element.findAttr(qualifiedName)
+    if i != -1:
+      element.delAttr(i)
     return ok(false)
   return ok(true)
 
 proc value(attr: Attr, s: string) {.jsfset.} =
-  attr.value = s
-  if attr.ownerElement != nil:
-    attr.ownerElement.attr0(attr.name, s)
+  attr.ownerElement.attr(attr.name, s)
 
 proc setNamedItem(map: NamedNodeMap, attr: Attr): DOMResult[Attr]
     {.jsfunc.} =
-  if attr.ownerElement != nil and attr.ownerElement != map.element:
+  if attr.ownerElement == map.element:
+    # Setting attr on its owner element does nothing, since the "get an
+    # attribute by namespace and local name" step is used for retrieval
+    # (which will always return self).
+    return
+  if attr.jsOwnerElement != nil:
     return errDOMException("Attribute is currently in use",
       "InUseAttributeError")
-  if attr.name in map.element.attrs:
-    return ok(attr)
-  let i = map.findAttr(attr.name)
+  let i = map.element.findAttrNS(attr.data.namespace, attr.data.localName)
+  attr.ownerElement = map.element
   if i != -1:
-    result = ok(map.attrlist[i])
-    map.attrlist.delete(i)
-  else:
-    result = ok(nil)
-  map.element.attrs[attr.name] = attr.value
-  map.attrlist.add(attr)
+    map.element.attrs[i] = attr.data
+    return ok(attr)
+  map.element.attrs.add(attr.data)
+  return ok(nil)
 
 proc setNamedItemNS(map: NamedNodeMap, attr: Attr): DOMResult[Attr]
     {.jsfunc.} =
@@ -2628,19 +2863,19 @@ proc setNamedItemNS(map: NamedNodeMap, attr: Attr): DOMResult[Attr]
 
 proc removeNamedItem(map: NamedNodeMap, qualifiedName: string):
     DOMResult[Attr] {.jsfunc.} =
-  let i = map.findAttr(qualifiedName)
+  let i = map.element.findAttr(qualifiedName)
   if i != -1:
-    let attr = map.attrlist[i]
-    map.element.delAttr(i)
+    let attr = map.getAttr(i)
+    map.element.delAttr(i, keep = true)
     return ok(attr)
   return errDOMException("Item not found", "NotFoundError")
 
 proc removeNamedItemNS(map: NamedNodeMap, namespace, localName: string):
     DOMResult[Attr] {.jsfunc.} =
-  let i = map.findAttrNS(namespace, localName)
+  let i = map.element.findAttrNS(namespace, localName)
   if i != -1:
-    let attr = map.attrlist[i]
-    map.element.delAttr(i)
+    let attr = map.getAttr(i)
+    map.element.delAttr(i, keep = true)
     return ok(attr)
   return errDOMException("Item not found", "NotFoundError")
 
@@ -2663,7 +2898,7 @@ proc remove*(node: Node, suppressObservers: bool) =
   node.parentNode = nil
   node.root = nil
   node.index = -1
-  if node.nodeType == ELEMENT_NODE:
+  if node of Element:
     if Element(node).tagType in {TAG_STYLE, TAG_LINK} and node.document != nil:
       node.document.cachedSheetsInvalid = true
 
@@ -2681,7 +2916,7 @@ proc adopt(document: Document, node: Node) =
     #TODO shadow root
     for desc in node.descendants:
       desc.document_internal = document
-      if desc.nodeType == ELEMENT_NODE:
+      if desc of Element:
         for attr in Element(desc).attributes.attrlist:
           attr.document_internal = document
     #TODO custom elements
@@ -2766,7 +3001,7 @@ proc resetFormOwner(element: FormAssociatedElement) =
       element.setForm(HTMLFormElement(form))
 
 proc insertionSteps(insertedNode: Node) =
-  if insertedNode.nodeType == ELEMENT_NODE:
+  if insertedNode of Element:
     let element = Element(insertedNode)
     let tagType = element.tagType
     case tagType
@@ -2787,8 +3022,14 @@ proc insertionSteps(insertedNode: Node) =
         return
       element.resetFormOwner()
 
+func isValidParent(node: Node): bool =
+  return node of Element or node of Document or node of DocumentFragment
+
+func isValidChild(node: Node): bool =
+  return node.isValidParent or node of DocumentType or node of CharacterData
+
 func checkParentValidity(parent: Node): Err[DOMException] =
-  if parent.nodeType in {DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE}:
+  if parent.isValidParent():
     return ok()
   const msg = "Parent must be a document, a document fragment, or an element."
   return errDOMException(msg, "HierarchyRequestError")
@@ -2803,40 +3044,37 @@ func preInsertionValidity*(parent, node, before: Node): Err[DOMException] =
   if before != nil and before.parentNode != parent:
     return errDOMException("Reference node is not a child of parent",
       "NotFoundError")
-  if node.nodeType notin {DOCUMENT_FRAGMENT_NODE, DOCUMENT_TYPE_NODE,
-      ELEMENT_NODE} + CharacterDataNodes:
-    return errDOMException("Cannot insert node type",
-      "HierarchyRequestError")
-  if node.nodeType == TEXT_NODE and parent.nodeType == DOCUMENT_NODE:
+  if not node.isValidChild():
+    return errDOMException("Node is not a valid child", "HierarchyRequestError")
+  if node of Text and parent of Document:
     return errDOMException("Cannot insert text into document",
       "HierarchyRequestError")
-  if node.nodeType == DOCUMENT_TYPE_NODE and parent.nodeType != DOCUMENT_NODE:
+  if node of DocumentType and not (parent of Document):
     return errDOMException("Document type can only be inserted into document",
       "HierarchyRequestError")
-  if parent.nodeType == DOCUMENT_NODE:
-    case node.nodeType
-    of DOCUMENT_FRAGMENT_NODE:
-      let elems = node.countChildren(ELEMENT_NODE)
-      if elems > 1 or node.hasChild(TEXT_NODE):
+  if parent of Document:
+    if node of DocumentFragment:
+      let elems = node.countChildren(Element)
+      if elems > 1 or node.hasChild(Text):
         return errDOMException("Document fragment has invalid children",
           "HierarchyRequestError")
-      elif elems == 1 and (parent.hasChild(ELEMENT_NODE) or
-          before != nil and (before.nodeType == DOCUMENT_TYPE_NODE or
-          before.hasNextSibling(DOCUMENT_TYPE_NODE))):
+      elif elems == 1 and (parent.hasChild(Element) or
+          before != nil and (before of DocumentType or
+          before.hasNextSibling(DocumentType))):
         return errDOMException("Document fragment has invalid children",
           "HierarchyRequestError")
-    of ELEMENT_NODE:
-      if parent.hasChild(ELEMENT_NODE):
+    elif node of Element:
+      if parent.hasChild(Element):
         return errDOMException("Document already has an element child",
           "HierarchyRequestError")
-      elif before != nil and (before.nodeType == DOCUMENT_TYPE_NODE or
-            before.hasNextSibling(DOCUMENT_TYPE_NODE)):
+      elif before != nil and (before of DocumentType or
+            before.hasNextSibling(DocumentType)):
         return errDOMException("Cannot insert element before document type",
           "HierarchyRequestError")
-    of DOCUMENT_TYPE_NODE:
-      if parent.hasChild(DOCUMENT_TYPE_NODE) or
-          before != nil and before.hasPreviousSibling(ELEMENT_NODE) or
-          before == nil and parent.hasChild(ELEMENT_NODE):
+    elif node of DocumentType:
+      if parent.hasChild(DocumentType) or
+          before != nil and before.hasPreviousSibling(Element) or
+          before == nil and parent.hasChild(Element):
         const msg = "Cannot insert document type before an element node"
         return errDOMException(msg, "HierarchyRequestError")
     else: discard
@@ -2857,28 +3095,29 @@ proc insertNode(parent, node, before: Node) =
   node.parentNode = parent
   node.invalidateCollections()
   parent.invalidateCollections()
-  if node.nodeType == ELEMENT_NODE:
+  if node of Element:
     if Element(node).tagType in {TAG_STYLE, TAG_LINK} and node.document != nil:
       node.document.cachedSheetsInvalid = true
-  if node.nodeType == ELEMENT_NODE:
     #TODO shadow root
     insertionSteps(node)
 
 # WARNING ditto
 proc insert*(parent, node, before: Node, suppressObservers = false) =
-  let nodes = if node.nodeType == DOCUMENT_FRAGMENT_NODE: node.childList
-  else: @[node]
+  let nodes = if node of DocumentFragment:
+    node.childList
+  else:
+    @[node]
   let count = nodes.len
   if count == 0:
     return
-  if node.nodeType == DOCUMENT_FRAGMENT_NODE:
+  if node of DocumentFragment:
     for i in countdown(node.childList.high, 0):
       node.childList[i].remove(true)
     #TODO tree mutation record
   if before != nil:
     #TODO live ranges
     discard
-  if parent.nodeType == ELEMENT_NODE:
+  if parent of Element:
     Element(parent).invalid = true
   for node in nodes:
     insertNode(parent, node, before)
@@ -2918,43 +3157,36 @@ proc replace(parent, child, node: Node): Err[DOMException] =
   if child.parentNode != parent:
     return errDOMException("Node to replace is not a child of parent",
       "NotFoundError")
-  if node.nodeType notin {DOCUMENT_NODE, DOCUMENT_TYPE_NODE, ELEMENT_NODE} +
-      CharacterDataNodes:
-    return errDOMException("Replacement is not a valid replacement node type",
-      "HierarchyRequesError")
-  if node.nodeType == TEXT_NODE and parent.nodeType == DOCUMENT_NODE or
-      node.nodeType == DOCUMENT_TYPE_NODE and parent.nodeType != DOCUMENT_NODE:
+  if not node.isValidChild():
+    return errDOMException("Node is not a valid child", "HierarchyRequesError")
+  if node of Text and parent of Document or
+      node of DocumentType and not (parent of Document):
     return errDOMException("Replacement cannot be placed in parent",
       "HierarchyRequesError")
   let childNextSibling = child.nextSibling
   let childPreviousSibling = child.previousSibling
-  if parent.nodeType == DOCUMENT_NODE:
-    case node.nodeType
-    of DOCUMENT_FRAGMENT_NODE:
-      let elems = node.countChildren(ELEMENT_NODE)
-      if elems > 1 or node.hasChild(TEXT_NODE):
+  if parent of Document:
+    if node of DocumentFragment:
+      let elems = node.countChildren(Element)
+      if elems > 1 or node.hasChild(Text):
         return errDOMException("Document fragment has invalid children",
           "HierarchyRequestError")
-      elif elems == 1 and (parent.hasChildExcept(ELEMENT_NODE, child) or
-          childNextSibling != nil and
-          childNextSibling.nodeType == DOCUMENT_TYPE_NODE):
+      elif elems == 1 and (parent.hasChildExcept(Element, child) or
+          childNextSibling != nil and childNextSibling of DocumentType):
         return errDOMException("Document fragment has invalid children",
           "HierarchyRequestError")
-    of ELEMENT_NODE:
-      if parent.hasChildExcept(ELEMENT_NODE, child):
+    elif node of Element:
+      if parent.hasChildExcept(Element, child):
         return errDOMException("Document already has an element child",
           "HierarchyRequestError")
-      elif childNextSibling != nil and
-          childNextSibling.nodeType == DOCUMENT_TYPE_NODE:
+      elif childNextSibling != nil and childNextSibling of DocumentType:
         return errDOMException("Cannot insert element before document type ",
           "HierarchyRequestError")
-    of DOCUMENT_TYPE_NODE:
-      if parent.hasChildExcept(DOCUMENT_TYPE_NODE, child) or
-          childPreviousSibling != nil and
-          childPreviousSibling.nodeType == DOCUMENT_TYPE_NODE:
+    elif node of DocumentType:
+      if parent.hasChildExcept(DocumentType, child) or
+          childPreviousSibling != nil and childPreviousSibling of DocumentType:
         const msg = "Cannot insert document type before an element node"
         return errDOMException(msg, "HierarchyRequestError")
-    else: discard
   let referenceChild = if childNextSibling == node:
     node.nextSibling
   else:
@@ -2972,7 +3204,7 @@ proc replaceAll(parent, node: Node) =
     child.remove(true)
   assert parent != node
   if node != nil:
-    if node.nodeType == DOCUMENT_FRAGMENT_NODE:
+    if node of DocumentFragment:
       var addedNodes = node.childList # copy
       for child in addedNodes:
         parent.append(child)
@@ -2984,18 +3216,16 @@ proc createTextNode*(document: Document, data: string): Text {.jsfunc.} =
   return newText(document, data)
 
 proc textContent*(node: Node, data: Option[string]) {.jsfset.} =
-  case node.nodeType
-  of DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE:
+  if node of Element or node of DocumentFragment:
     let x = if data.isSome:
       node.document.createTextNode(data.get)
     else:
       nil
     node.replaceAll(x)
-  of ATTRIBUTE_NODE:
-    value(Attr(node), data.get(""))
-  of TEXT_NODE, COMMENT_NODE:
+  elif node of CharacterData:
     CharacterData(node).data = data.get("")
-  else: discard
+  elif node of Attr:
+    value(Attr(node), data.get(""))
 
 proc reset*(form: HTMLFormElement) =
   for control in form.controls:
@@ -3135,6 +3365,9 @@ proc execute*(element: HTMLScriptElement) =
   if element.scriptResult.t == RESULT_NULL:
     #TODO fire error event
     return
+  let needsInc = element.external or element.ctype == MODULE
+  if needsInc:
+    inc document.ignoreDestructiveWrites
   case element.ctype
   of CLASSIC:
     let oldCurrentScript = document.currentScript
@@ -3153,6 +3386,8 @@ proc execute*(element: HTMLScriptElement) =
           ss.readAll())
     document.currentScript = oldCurrentScript
   else: discard #TODO
+  if needsInc:
+    dec document.ignoreDestructiveWrites
 
 # https://html.spec.whatwg.org/multipage/scripting.html#prepare-the-script-element
 proc prepare*(element: HTMLScriptElement) =
@@ -3218,7 +3453,7 @@ proc prepare*(element: HTMLScriptElement) =
     if src == "":
       #TODO fire error event
       return
-    element.fromAnExternalFile = true
+    element.external = true
     let url = element.document.parseURL(src)
     if url.isNone:
       #TODO fire error event
@@ -3285,9 +3520,9 @@ proc createElement(document: Document, localName: string):
     return errDOMException("Invalid character in element name",
       "InvalidCharacterError")
   let localName = if not document.isxml:
-    localName.toLowerAscii()
+    document.toAtom(localName.toLowerAscii())
   else:
-    localName
+    document.toAtom(localName)
   let namespace = if not document.isxml: #TODO or content type is application/xhtml+xml
     Namespace.HTML
   else:
@@ -3307,11 +3542,11 @@ proc createDocumentType(implementation: ptr DOMImplementation, qualifiedName,
   let document = implementation.document
   return ok(document.newDocumentType(qualifiedName, publicId, systemId))
 
-proc createHTMLDocument(implementation: ptr DOMImplementation, title =
-    none(string)): Document {.jsfunc.} =
-  let doc = newDocument()
+proc createHTMLDocument(ctx: JSContext, implementation: ptr DOMImplementation,
+    title = none(string)): Document {.jsfunc.} =
+  let doc = newDocument(ctx)
   doc.contentType = "text/html"
-  doc.append(doc.newDocumentType("html"))
+  doc.append(doc.newDocumentType("html", "", ""))
   let html = doc.newHTMLElement(TAG_HTML, Namespace.HTML)
   doc.append(html)
   let head = doc.newHTMLElement(TAG_HEAD, Namespace.HTML)
@@ -3347,14 +3582,13 @@ proc createProcessingInstruction(document: Document, target, data: string):
       "InvalidCharacterError")
   return ok(newProcessingInstruction(document, target, data))
 
-func clone(node: Node, document = none(Document), deep = false): Node =
+proc clone(node: Node, document = none(Document), deep = false): Node =
   let document = document.get(node.document)
-  let copy = case node.nodeType
-  of ELEMENT_NODE:
+  let copy = if node of Element:
     #TODO is value
     let element = Element(node)
     let x = document.newHTMLElement(element.localName, element.namespace,
-      element.namespacePrefix, element.tagType, element.attrs)
+      element.namespacePrefix, element.attrs)
     #TODO namespaced attrs?
     # Cloning steps
     if x.tagType == TAG_SCRIPT:
@@ -3369,43 +3603,50 @@ func clone(node: Node, document = none(Document), deep = false): Node =
       x.checked = element.checked
       #TODO dirty checkedness flag
     Node(x)
-  of ATTRIBUTE_NODE:
+  elif node of Attr:
     let attr = Attr(node)
-    let x = document.newAttr(attr.localName, attr.value, attr.prefix,
-      attr.namespaceURI)
+    let data = attr.data
+    let x = Attr(
+      ownerElement: AttrDummyElement(
+        document_internal: attr.ownerElement.document,
+        index: -1,
+        attrs: @[data]
+      ),
+      dataIdx: 0
+    )
     Node(x)
-  of TEXT_NODE:
+  elif node of Text:
     let text = Text(node)
     let x = document.newText(text.data)
     Node(x)
-  of CDATA_SECTION_NODE:
+  elif node of CDATASection:
     let x = document.newCDATASection("")
     #TODO is this really correct??
     # really, I don't know. only relevant with xhtml anyway...
     Node(x)
-  of COMMENT_NODE:
+  elif node of Comment:
     let comment = Comment(node)
     let x = document.newComment(comment.data)
     Node(x)
-  of PROCESSING_INSTRUCTION_NODE:
+  elif node of ProcessingInstruction:
     let procinst = ProcessingInstruction(node)
     let x = document.newProcessingInstruction(procinst.target, procinst.data)
     Node(x)
-  of DOCUMENT_NODE:
+  elif node of Document:
     let document = Document(node)
-    let x = newDocument()
+    let x = newDocument(document.factory)
     x.charset = document.charset
     x.contentType = document.contentType
     x.url = document.url
     x.isxml = document.isxml
     x.mode = document.mode
     Node(x)
-  of DOCUMENT_TYPE_NODE:
+  elif node of DocumentType:
     let doctype = DocumentType(node)
     let x = document.newDocumentType(doctype.name, doctype.publicId,
       doctype.systemId)
     Node(x)
-  of DOCUMENT_FRAGMENT_NODE:
+  elif node of DocumentFragment:
     let x = document.newDocumentFragment()
     Node(x)
   else:
@@ -3416,7 +3657,7 @@ func clone(node: Node, document = none(Document), deep = false): Node =
       copy.append(child.clone(deep = true))
   return copy
 
-func cloneNode(node: Node, deep = false): Node {.jsfunc.} =
+proc cloneNode(node: Node, deep = false): Node {.jsfunc.} =
   #TODO shadow root
   return node.clone(deep = deep)
 
@@ -3495,7 +3736,9 @@ proc jsReflectSet(ctx: JSContext, this, val: JSValue, magic: cint): JSValue {.cd
       if x.get:
         element.attr(entry.attrname, "")
       else:
-        element.delAttr(entry.attrname)
+        let i = element.findAttr(entry.attrname)
+        if i != -1:
+          element.delAttr(i)
   of REFLECT_LONG:
     let x = fromJS[int32](ctx, val)
     if x.isSome:
@@ -3575,11 +3818,11 @@ proc outerHTML(element: Element, s: string): Err[DOMException] {.jsfset.} =
   let parent0 = element.parentNode
   if parent0 == nil:
     return ok()
-  if parent0.nodeType == DOCUMENT_NODE:
+  if parent0 of Document:
     let ex = newDOMException("outerHTML is disallowed for Document children",
       "NoModificationAllowedError")
     return err(ex)
-  let parent = if parent0.nodeType == DOCUMENT_FRAGMENT_NODE:
+  let parent = if parent0 of DocumentFragment:
     element.document.newHTMLElement(TAG_BODY)
   else:
     # neither a document, nor a document fragment => parent must be an
@@ -3594,8 +3837,7 @@ proc insertAdjacentHTML(element: Element, position, text: string):
   #TODO enumize position
   let ctx0 = case position
   of "beforebegin", "afterend":
-    if element.parentNode.nodeType == DOCUMENT_NODE or
-        element.parentNode == nil:
+    if element.parentNode of Document or element.parentNode == nil:
       return errDOMException("Parent is not a valid element",
         "NoModificationAllowedError")
     element.parentNode
@@ -3604,7 +3846,7 @@ proc insertAdjacentHTML(element: Element, position, text: string):
   else:
     return errDOMException("Invalid position", "SyntaxError")
   let document = ctx0.document
-  let ctx = if ctx0.nodeType != ELEMENT_NODE or not document.isxml or
+  let ctx = if not (ctx0 of Element) or not document.isxml or
       Element(ctx0).namespace == Namespace.HTML:
     document.newHTMLElement(TAG_BODY)
   else:
diff --git a/src/html/enums.nim b/src/html/enums.nim
index 24f496bb..67ae4e76 100644
--- a/src/html/enums.nim
+++ b/src/html/enums.nim
@@ -16,6 +16,21 @@ type
   ButtonType* = enum
     BUTTON_SUBMIT, BUTTON_RESET, BUTTON_BUTTON
 
+  NodeType* = enum
+    ELEMENT_NODE = 1,
+    ATTRIBUTE_NODE = 2,
+    TEXT_NODE = 3,
+    CDATA_SECTION_NODE = 4,
+    ENTITY_REFERENCE_NODE = 5,
+    ENTITY_NODE = 6
+    PROCESSING_INSTRUCTION_NODE = 7,
+    COMMENT_NODE = 8,
+    DOCUMENT_NODE = 9,
+    DOCUMENT_TYPE_NODE = 10,
+    DOCUMENT_FRAGMENT_NODE = 11,
+    NOTATION_NODE = 12
+
+
 #TODO support all the other ones
 const SupportedFormAssociatedElements* = {
   TAG_BUTTON, TAG_INPUT, TAG_SELECT, TAG_TEXTAREA
@@ -31,7 +46,8 @@ const AutocapitalizeInheritingElements* = {
 
 const LabelableElements* = {
   # input only if type not hidden
-  TAG_BUTTON, TAG_INPUT, TAG_METER, TAG_OUTPUT, TAG_PROGRESS, TAG_SELECT, TAG_TEXTAREA
+  TAG_BUTTON, TAG_INPUT, TAG_METER, TAG_OUTPUT, TAG_PROGRESS, TAG_SELECT,
+  TAG_TEXTAREA
 }
 
 # https://html.spec.whatwg.org/multipage/syntax.html#void-elements
@@ -40,6 +56,10 @@ const VoidElements* = {
   TAG_LINK, TAG_META, TAG_SOURCE, TAG_TRACK, TAG_WBR
 }
 
+const ResettableElements* = {
+  TAG_INPUT, TAG_OUTPUT, TAG_SELECT, TAG_TEXTAREA
+}
+
 func getInputTypeMap(): Table[string, InputType] =
   for i in InputType:
     let enumname = $InputType(i)
diff --git a/src/html/env.nim b/src/html/env.nim
index f462c347..9eff5e03 100644
--- a/src/html/env.nim
+++ b/src/html/env.nim
@@ -3,6 +3,7 @@ import std/streams
 
 import bindings/quickjs
 import display/winattrs
+import html/catom
 import html/chadombuilder
 import html/dom
 import html/event
@@ -176,8 +177,8 @@ proc runJSJobs*(window: Window) =
   window.jsrt.runJSJobs(window.console.err)
 
 proc newWindow*(scripting: bool, selector: Selector[int],
-    attrs: WindowAttributes, navigate: proc(url: URL) = nil,
-    loader = none(FileLoader)): Window =
+    attrs: WindowAttributes, factory: CAtomFactory,
+    navigate: proc(url: URL) = nil, loader = none(FileLoader)): Window =
   let err = newFileStream(stderr)
   let window = Window(
     attrs: attrs,
@@ -187,7 +188,8 @@ proc newWindow*(scripting: bool, selector: Selector[int],
     settings: EnvironmentSettings(
       scripting: scripting
     ),
-    navigate: navigate
+    navigate: navigate,
+    factory: factory
   )
   window.location = window.newLocation()
   if scripting:
diff --git a/src/server/buffer.nim b/src/server/buffer.nim
index 093bc2d7..abb116c6 100644
--- a/src/server/buffer.nim
+++ b/src/server/buffer.nim
@@ -18,6 +18,7 @@ import css/sheet
 import css/stylednode
 import css/values
 import display/winattrs
+import html/catom
 import html/chadombuilder
 import html/dom
 import html/enums
@@ -120,6 +121,9 @@ type
     estream: Stream # error stream
     ishtml: bool
     ssock: ServerSocket
+    factory: CAtomFactory
+    uastyle: CSSStylesheet
+    quirkstyle: CSSStylesheet
 
   InterfaceOpaque = ref object
     stream: Stream
@@ -626,19 +630,14 @@ proc gotoAnchor*(buffer: Buffer): Opt[tuple[x, y: int]] {.proxy.} =
         return ok((format.pos, y))
   return err()
 
-const css = staticRead"res/ua.css"
-let uastyle = css.parseStylesheet()
-const quirk = css & staticRead"res/quirk.css"
-let quirkstyle = quirk.parseStylesheet()
-
 proc do_reshape(buffer: Buffer) =
   if buffer.ishtml:
     if buffer.document == nil:
       return # not parsed yet, nothing to render
     let uastyle = if buffer.document.mode != QUIRKS:
-      uastyle
+      buffer.uastyle
     else:
-      quirkstyle
+      buffer.quirkstyle
     let styledRoot = buffer.document.applyStylesheets(uastyle,
       buffer.userstyle, buffer.prevstyled)
     buffer.lines = renderDocument(styledRoot, buffer.attrs)
@@ -725,7 +724,7 @@ proc loadResource(buffer: Buffer, elem: HTMLLinkElement): EmptyPromise =
           #TODO non-utf-8 css
           let ds = newDecoderStream(ss, cs = CHARSET_UTF_8)
           let source = newEncoderStream(ds, cs = CHARSET_UTF_8)
-          elem.sheet = parseStylesheet(source))
+          elem.sheet = parseStylesheet(source, buffer.factory))
 
 proc loadResource(buffer: Buffer, elem: HTMLImageElement): EmptyPromise =
   let document = buffer.document
@@ -783,6 +782,33 @@ type ConnectResult* = object
   referrerpolicy*: Option[ReferrerPolicy]
   charset*: Charset
 
+proc setHTML(buffer: Buffer, ishtml: bool) =
+  buffer.ishtml = ishtml
+  if ishtml:
+    let factory = newCAtomFactory()
+    buffer.factory = factory
+    if buffer.config.scripting:
+      buffer.window = newWindow(
+        buffer.config.scripting,
+        buffer.selector,
+        buffer.attrs,
+        factory,
+        proc(url: URL) = buffer.navigate(url),
+        some(buffer.loader)
+      )
+    else:
+      buffer.window = newWindow(
+        buffer.config.scripting,
+        buffer.selector,
+        buffer.attrs,
+        buffer.factory
+      )
+    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)
+
 proc connect*(buffer: Buffer): ConnectResult {.proxy.} =
   if buffer.connected:
     return ConnectResult(invalid: true)
@@ -841,7 +867,7 @@ proc connect*(buffer: Buffer): ConnectResult {.proxy.} =
         buffer.loader.setReferrerPolicy(referrerpolicy.get)
   buffer.connected = true
   let contentType = buffer.source.contentType.get("")
-  buffer.ishtml = contentType == "text/html"
+  buffer.setHTML(contentType == "text/html")
   return ConnectResult(
     charset: charset,
     needsAuth: needsAuth,
@@ -904,7 +930,7 @@ proc readFromFd*(buffer: Buffer, fd: FileHandle, ishtml: bool) {.proxy.} =
     contentType: some(contentType),
     charset: buffer.source.charset
   )
-  buffer.ishtml = ishtml
+  buffer.setHTML(ishtml)
   discard fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) or O_NONBLOCK)
   buffer.istream = newPosixStream(fd)
   buffer.fd = fd
@@ -912,7 +938,7 @@ proc readFromFd*(buffer: Buffer, fd: FileHandle, ishtml: bool) {.proxy.} =
 
 proc setContentType*(buffer: Buffer, contentType: string) {.proxy.} =
   buffer.source.contentType = some(contentType)
-  buffer.ishtml = contentType == "text/html"
+  buffer.setHTML(contentType == "text/html")
 
 # 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
@@ -1114,11 +1140,13 @@ proc finishLoad(buffer: Buffer): EmptyPromise =
   if buffer.ishtml:
     buffer.sstream.setPosition(0)
     buffer.available = 0
-    if buffer.window == nil:
-      buffer.window = newWindow(buffer.config.scripting, buffer.selector,
-        buffer.attrs)
-    let document = parseHTML(buffer.sstream, charsets = buffer.charsets,
-      window = buffer.window, url = buffer.url)
+    let document = parseHTML(
+      buffer.sstream,
+      charsets = buffer.charsets,
+      window = buffer.window,
+      url = buffer.url,
+      factory = buffer.factory
+    )
     buffer.document = document
     document.readyState = READY_STATE_INTERACTIVE
     buffer.state = LOADING_RESOURCES
@@ -1211,12 +1239,14 @@ proc cancel*(buffer: Buffer): int {.proxy.} =
   if buffer.ishtml:
     buffer.sstream.setPosition(0)
     buffer.available = 0
-    if buffer.window == nil:
-      buffer.window = newWindow(buffer.config.scripting, buffer.selector,
-        buffer.attrs)
-    buffer.document = parseHTML(buffer.sstream,
-      charsets = buffer.charsets, window = buffer.window,
-      url = buffer.url, canReinterpret = false)
+    buffer.document = parseHTML(
+      buffer.sstream,
+      charsets = buffer.charsets,
+      window = buffer.window,
+      url = buffer.url,
+      factory = buffer.factory,
+      seekable = false
+    )
     buffer.do_reshape()
   return buffer.lines.len
 
@@ -1811,7 +1841,6 @@ proc launchBuffer*(config: BufferConfig, source: BufferSource,
   let socks = ssock.acceptSocketStream()
   let buffer = Buffer(
     alive: true,
-    userstyle: parseStylesheet(config.userstyle),
     attrs: attrs,
     config: config,
     loader: loader,
@@ -1835,9 +1864,6 @@ proc launchBuffer*(config: BufferConfig, source: BufferSource,
     buffer.selector.registerHandle(fd, {Read}, 0)
   loader.unregisterFun = proc(fd: int) =
     buffer.selector.unregister(fd)
-  if buffer.config.scripting:
-    buffer.window = newWindow(buffer.config.scripting, buffer.selector,
-      buffer.attrs, proc(url: URL) = buffer.navigate(url), some(buffer.loader))
   buffer.selector.registerHandle(buffer.rfd, {Read}, 0)
   buffer.runBuffer()
   buffer.cleanup()
diff --git a/src/xhr/formdata.nim b/src/xhr/formdata.nim
index e5cc5aa0..7d83d5b2 100644
--- a/src/xhr/formdata.nim
+++ b/src/xhr/formdata.nim
@@ -137,7 +137,7 @@ proc constructEntryList*(form: HTMLFormElement, submitter: Element = nil,
     if field.tagType == TAG_SELECT:
       let field = HTMLSelectElement(field)
       for option in field.options:
-        if option.selected or option.disabled:
+        if option.selected or option.isDisabled:
           entrylist.add((name, option.value))
     elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType in {INPUT_CHECKBOX, INPUT_RADIO}:
       let value = if field.attr("value") != "":
diff --git a/todo b/todo
index eed500d4..8f2b6d1b 100644
--- a/todo
+++ b/todo
@@ -56,6 +56,10 @@ external:
 - history, bookmarks (w3m format)
 - save buffer (source, output)
 - edit buffer (local file, or whatever we downloaded)
+dom:
+- atomize `id'
+	* maybe also add `name' attr atom?
+- add attr name enum (like TagType)
 javascript:
 - important: callbacks should not leak memory
 - add support for JS mixins
@@ -66,6 +70,7 @@ javascript:
 - implement ReadableStream, XHR
 - separate console for each buffer
 - buffer selection
+- errTypeError etc templates
 layout engine:
 - box borders
 	* will probably need special treatment, as borders must round to