about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2023-07-09 20:11:05 +0200
committerbptato <nincsnevem662@gmail.com>2023-07-09 20:12:52 +0200
commitffd4414ecf579802d94da3535361c61e24b5d954 (patch)
tree18c4a4efb79e0ca62ec8886198adfd46375c2df8 /src
parent1b5d94a656096730a0d780ee2e1aece846e35fa5 (diff)
downloadchawan-ffd4414ecf579802d94da3535361c61e24b5d954.tar.gz
htmlparser: decouple from DOM
Instead of directly appending nodes to the DOM, use a DOMBuilder
interface.
Diffstat (limited to 'src')
-rw-r--r--src/buffer/buffer.nim2
-rw-r--r--src/display/client.nim2
-rw-r--r--src/html/chadombuilder.nim204
-rw-r--r--src/html/dom.nim25
-rw-r--r--src/html/env.nim2
-rw-r--r--src/html/htmlparser.nim1409
-rw-r--r--src/html/tags.nim12
-rw-r--r--src/render/renderdocument.nim1
8 files changed, 1113 insertions, 544 deletions
diff --git a/src/buffer/buffer.nim b/src/buffer/buffer.nim
index d9060d4c..d322c03e 100644
--- a/src/buffer/buffer.nim
+++ b/src/buffer/buffer.nim
@@ -19,9 +19,9 @@ import css/stylednode
 import css/values
 import data/charset
 import encoding/decoderstream
+import html/chadombuilder
 import html/dom
 import html/env
-import html/htmlparser
 import html/tags
 import img/png
 import io/loader
diff --git a/src/display/client.nim b/src/display/client.nim
index f7e89b78..aa36cdac 100644
--- a/src/display/client.nim
+++ b/src/display/client.nim
@@ -21,9 +21,9 @@ import css/sheet
 import data/charset
 import display/pager
 import display/term
+import html/chadombuilder
 import html/dom
 import html/event
-import html/htmlparser
 import io/headers
 import io/lineedit
 import io/loader
diff --git a/src/html/chadombuilder.nim b/src/html/chadombuilder.nim
new file mode 100644
index 00000000..5c7b1770
--- /dev/null
+++ b/src/html/chadombuilder.nim
@@ -0,0 +1,204 @@
+import deques
+import streams
+
+import data/charset
+import html/dom
+import html/htmlparser
+import html/tags
+import js/javascript
+import types/url
+
+# DOMBuilder implementation for Chawan.
+
+type
+  ChaDOMBuilder = ref object of DOMBuilder[Node]
+    isFragment: bool
+
+type DOMParser = ref object # JS interface
+
+jsDestructor(DOMParser)
+
+template getDocument(dombuilder: ChaDOMBuilder): Document =
+  cast[Document](dombuilder.document)
+
+proc finish(builder: DOMBuilder[Node]) =
+  let builder = cast[ChaDOMBuilder](builder)
+  let document = builder.getDocument()
+  while document.scriptsToExecOnLoad.len > 0:
+    #TODO spin event loop
+    let script = document.scriptsToExecOnLoad.popFirst()
+    script.execute()
+  #TODO events
+
+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 =
+  return HTMLTemplateElement(handle).content
+
+proc getTagType(builder: DOMBuilder[Node], handle: Node): TagType =
+  return Element(handle).tagType
+
+proc getLocalName(builder: DOMBuilder[Node], handle: Node): string =
+  return Element(handle).localName
+
+proc getNamespace(builder: DOMBuilder[Node], 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():
+    element.resetElement()
+  if tagType == TAG_SCRIPT:
+    let script = HTMLScriptElement(element)
+    script.parserDocument = document
+    script.forceAsync = false
+    if builder.isFragment:
+      script.alreadyStarted = true
+      #TODO document.write (?)
+  return element
+
+proc createComment(builder: DOMBuilder[Node], text: string): Node =
+  let builder = cast[ChaDOMBuilder](builder)
+  return builder.getDocument().createComment(text)
+
+proc createDocumentType(builder: DOMBuilder[Node], 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
+  else:
+    parent.lastChild
+  if prevSibling != nil and prevSibling.nodeType == TEXT_NODE:
+    Text(prevSibling).data &= text
+  else:
+    let text = builder.getDocument().createTextNode(text)
+    discard parent.insertBefore(text, before)
+
+proc reparent(builder: DOMBuilder[Node], child, newParent: Node) =
+  child.remove(true)
+  newParent.append(child)
+
+proc addAttrsIfMissing(builder: DOMBuilder[Node], element: Node,
+    attrs: Table[string, string]) =
+  let element = Element(element)
+  for k, v in attrs:
+    if not element.attrb(k):
+      element.attr(k, v)
+
+proc setScriptAlreadyStarted(builder: DOMBuilder[Node], script: Node) =
+  HTMLScriptElement(script).alreadyStarted = true
+
+proc associateWithForm(builder: DOMBuilder[Node], element, form: Node) =
+  if form.inSameTree(element):
+    #TODO remove following test eventually
+    if Element(element).tagType in SupportedFormAssociatedElements:
+      let element = FormAssociatedElement(element)
+      element.setForm(HTMLFormElement(form))
+      element.parserInserted = true
+
+proc elementPopped(builder: DOMBuilder[Node], element: Node) =
+  let builder = cast[ChaDOMBuilder](builder)
+  let document = builder.getDocument()
+  let element = Element(element)
+  if element.tagType == TAG_TEXTAREA:
+    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): ChaDOMBuilder =
+  let document = newDocument()
+  document.contentType = "text/html"
+  document.url = url
+  if window != nil:
+    document.window = window
+    window.document = document
+  return ChaDOMBuilder(
+    document: document,
+    finish: finish,
+    parseError: parseError,
+    setQuirksMode: setQuirksMode,
+    setCharacterSet: setCharacterset,
+    elementPopped: elementPopped,
+    getTemplateContent: getTemplateContent,
+    getTagType: getTagType,
+    getLocalName: getLocalName,
+    getNamespace: getNamespace,
+    createElement: createElement,
+    createComment: createComment,
+    createDocumentType: createDocumentType,
+    insertBefore: insertBefore,
+    insertText: insertText,
+    reparent: reparent,
+    addAttrsIfMissing: addAttrsIfMissing,
+    setScriptAlreadyStarted: setScriptAlreadyStarted,
+    associateWithForm: associateWithForm,
+    #TODO isSVGIntegrationPoint (SVG support)
+  )
+
+#TODO we shouldn't allow passing nil to window
+proc parseHTML*(inputStream: Stream, window: Window, url: URL,
+    charsets: seq[Charset] = @[], canReinterpret = true): Document =
+  let builder = newChaDOMBuilder(url, window)
+  let opts = HTML5ParserOpts[Node](
+    isIframeSrcdoc: false, #TODO?
+    scripting: window != nil and window.settings.scripting,
+    canReinterpret: canReinterpret,
+    charsets: charsets
+  )
+  builder.isFragment = opts.ctx.isSome
+  parseHTML(inputStream, builder, opts)
+  return Document(builder.document)
+
+proc newDOMParser(): DOMParser {.jsctor.} =
+  new(result)
+
+proc parseFromString(parser: DOMParser, str: string, t: string):
+    Result[Document, JSError] {.jsfunc.} =
+  case t
+  of "text/html":
+    #TODO window should be stored in DOMParser somehow. Setting it to nil
+    # is wrong.
+    let url = newURL("about:blank").get
+    let res = parseHTML(newStringStream(str), Window(nil), url)
+    return ok(res)
+  of "text/xml", "application/xml", "application/xhtml+xml", "image/svg+xml":
+    return err(newInternalError("XML parsing is not supported yet"))
+  else:
+    return err(newTypeError("Invalid mime type"))
+
+proc addHTMLModule*(ctx: JSContext) =
+  ctx.registerType(DOMParser)
diff --git a/src/html/dom.nim b/src/html/dom.nim
index e203dda1..b20567fb 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -42,18 +42,6 @@ type
     FORM_ENCODING_TYPE_MULTIPART = "multipart/form-data",
     FORM_ENCODING_TYPE_TEXT_PLAIN = "text/plain"
 
-  QuirksMode* = enum
-    NO_QUIRKS, QUIRKS, LIMITED_QUIRKS
-
-  Namespace* = enum
-    NO_NAMESPACE = "",
-    HTML = "http://www.w3.org/1999/xhtml",
-    MATHML = "http://www.w3.org/1998/Math/MathML",
-    SVG = "http://www.w3.org/2000/svg",
-    XLINK = "http://www.w3.org/1999/xlink",
-    XML = "http://www.w3.org/XML/1998/namespace",
-    XMLNS = "http://www.w3.org/2000/xmlns/"
-
   ScriptType = enum
     NO_SCRIPTTYPE, CLASSIC, MODULE, IMPORTMAP
 
@@ -231,7 +219,7 @@ type
 
     id* {.jsget.}: string
     classList* {.jsget.}: DOMTokenList
-    attrs*: Table[string, string]
+    attrs: Table[string, string]
     attributes* {.jsget.}: NamedNodeMap
     hover*: bool
     invalid*: bool
@@ -1377,12 +1365,15 @@ func getter[T: int|string](map: NamedNodeMap, i: T): Option[Attr] {.jsgetprop.}
 func length(characterData: CharacterData): int {.jsfget.} =
   return characterData.data.utf16Len
 
+func scriptingEnabled*(document: Document): bool =
+  if document.window == nil:
+    return false
+  return document.window.settings.scripting
+
 func scriptingEnabled*(element: Element): bool =
   if element.document == nil:
     return false
-  if element.document.window == nil:
-    return false
-  return element.document.window.settings.scripting
+  return element.document.scriptingEnabled
 
 func form*(element: FormAssociatedElement): HTMLFormElement =
   case element.tagType
@@ -2513,7 +2504,7 @@ proc insert*(parent, node, before: Node) =
   for node in nodes:
     insertNode(parent, node, before)
 
-proc insertBefore(parent, node, before: Node): Result[Node, DOMException]
+proc insertBefore*(parent, node, before: Node): Result[Node, DOMException]
     {.jsfunc.} =
   ?parent.preInsertionValidity(node, before)
   let referenceChild = if before == node:
diff --git a/src/html/env.nim b/src/html/env.nim
index 06289c4e..97f202f9 100644
--- a/src/html/env.nim
+++ b/src/html/env.nim
@@ -1,9 +1,9 @@
 import selectors
 import streams
 
+import html/chadombuilder
 import html/dom
 import html/event
-import html/htmlparser
 import io/headers
 import io/loader
 import io/promise
diff --git a/src/html/htmlparser.nim b/src/html/htmlparser.nim
index bb7498c4..981f0112 100644
--- a/src/html/htmlparser.nim
+++ b/src/html/htmlparser.nim
@@ -1,55 +1,263 @@
-import deques
 import macros
 import options
 import sequtils
 import streams
-import strformat
 import strutils
 import tables
 import unicode
 
-import css/sheet
 import data/charset
 import encoding/decoderstream
-import html/dom
 import html/tags
 import html/htmltokenizer
-import js/javascript
-import types/url
 import utils/twtstr
 
+export macros, unicode
+
+# Heavily inspired by html5ever's TreeSink design.
+type
+  DOMBuilder*[Handle] = ref object of RootObj
+    document*: Handle
+    ## Must never be nil.
+    finish*: DOMBuilderFinish[Handle]
+    ## May be nil.
+    parseError*: DOMBuilderParseError[Handle]
+    ## May be nil.
+    setQuirksMode*: DOMBuilderSetQuirksMode[Handle]
+    ## May be nil.
+    setCharacterSet*: DOMBuilderSetCharacterSet[Handle]
+    ## May be nil.
+    elementPopped*: DOMBuilderElementPopped[Handle]
+    ## May be nil.
+    getTemplateContent*: DOMBuilderGetTemplateContent[Handle]
+    ## Must never be nil.
+    getLocalName*: DOMBuilderGetLocalName[Handle]
+    ## Must never be nil.
+    getTagType*: DOMBuilderGetTagType[Handle]
+    ## May be nil. (If nil, the parser falls back to getLocalName.)
+    getNamespace*: DOMBuilderGetNamespace[Handle]
+    ## May be nil. (If nil, the parser always uses the HTML namespace.)
+    createElement*: DOMBuilderCreateElement[Handle]
+    ## Must never be nil.
+    createComment*: DOMBuilderCreateComment[Handle]
+    ## Must never be nil.
+    createDocumentType*: DOMBuilderCreateDocumentType[Handle]
+    ## Must never be nil.
+    insertBefore*: DOMBuilderInsertBefore[Handle]
+    ## Must never be nil.
+    insertText*: DOMBuilderInsertText[Handle]
+    ## Must never be nil.
+    reparent*: DOMBuilderReparent[Handle]
+    ## Must never be nil.
+    addAttrsIfMissing*: DOMBuilderAddAttrsIfMissing[Handle]
+    ## May be nil. (If nil, some attributes may not be added to the HTML or
+    ## BODY element if more than one of their respecting opening tags exist.)
+    setScriptAlreadyStarted*: DOMBuilderSetScriptAlreadyStarted[Handle]
+    ## May be nil.
+    associateWithForm*: DOMBuilderAssociateWithForm[Handle]
+    ## May be nil.
+    isSVGIntegrationPoint*: DOMBuilderIsSVGIntegrationPoint[Handle]
+    ## May be nil. (If nil, the parser considers no Handle an SVG integration
+    ## point.)
+
+  HTML5ParserOpts*[Handle] = object
+    isIframeSrcdoc*: bool
+    ## Is the document an iframe srcdoc?
+    scripting*: bool
+    ## Is scripting enabled for this document?
+    canReinterpret*: bool
+    ## Can we try to parse the document again with a different character set?
+    ##
+    ## Note: this only works if inputStream is seekable, i.e.
+    ## inputStream.setPosition(0) must work correctly.
+    ##
+    ## Note 2: when this canReinterpret is false, confidence is set to
+    ## certain, no BOM sniffing is performed and meta charset tags are
+    ## disregarded. Expect this to change in the future.
+    charsets*: seq[Charset]
+    ## Fallback charsets. If empty, UTF-8 is used. In most cases, an empty
+    ## sequence or a single-element sequence consisting of a character set
+    ## chosen based on the user's locale will suffice.
+    ##
+    ## The parser goes through fallback charsets in the following order:
+    ## * A charset stack is initialized to `charsets`, reversed. This
+    ##   means that the first charset specified in `charsets` is on top of
+    ##   the stack. (e.g. say `charsets = @[CHARSET_UTF_16_LE, CHARSET_UTF_8]`,
+    ##   then utf-16-le is tried before utf-8.)
+    ## * BOM sniffing is attempted. If successful, confidence is set to
+    ##   certain and the resulting charset is pushed on top of the charset
+    ##   stack. (Continuing the previous example: if BOM sniffing determines
+    ##   the character encoding to be UTF-8, then utf-8 will be tried before
+    ##   utf-16-le.)
+    ## * If the charset stack is empty, UTF-8 is pushed on top.
+    ## * Attempt to parse the document with the first charset on top of
+    ##   the stack.
+    ## * If BOM sniffing was unsuccessful, and a <meta charset=...> tag
+    ##   is encountered, parsing is restarted with the specified charset.
+    ##   No further attempts are be made to detect the encoding, and decoder
+    ##   errors are signaled by U+FFFD replacement characters.
+    ## * Otherwise, each charset on the charset stack is tried until either no
+    ##   decoding errors are encountered, or only one charset is left. For
+    ##   the last charset, decoder errors are signaled by U+FFFD replacement
+    ##   characters.
+    ## TODO: changing the charset after a successful BOM sniffing probably
+    ## makes no sense whatsoever, as almost all supported encodings are
+    ## ASCII-compatible (and would thus error out on leading high bytes
+    ## anyways).
+    ctx*: Option[Handle]
+    ## Context element for fragment parsing. When set to some Handle,
+    ## the fragment case is used while parsing.
+
+  DOMBuilderFinish*[Handle] =
+    proc(builder: DOMBuilder[Handle]) {.nimcall.}
+      ## Parsing has finished.
+
+  DOMBuilderParseError*[Handle] =
+    proc(builder: DOMBuilder[Handle], message: string) {.nimcall.}
+      ## Parse error.
+
+  DOMBuilderSetQuirksMode*[Handle] =
+    proc(builder: DOMBuilder[Handle], quirksMode: QuirksMode) {.nimcall.}
+      ## Set quirks mode to either QUIRKS or LIMITED_QUIRKS. NO_QUIRKS
+      ## is the default and is therefore never used here.
+
+  DOMBuilderSetCharacterSet*[Handle] =
+    proc(builder: DOMBuilder[Handle], charset: Charset) {.nimcall.}
+      ## Set the recognized charset, if it differs from the initial input.
+
+  DOMBuilderElementPopped*[Handle] =
+    proc(builder: DOMBuilder[Handle], element: Handle) {.nimcall.}
+      ## Called when an element is popped from the stack of open elements
+      ## (i.e. when it has been closed.)
+
+  DOMBuilderGetTemplateContent*[Handle] =
+    proc(builder: DOMBuilder[Handle], handle: Handle): Handle {.nimcall.}
+      ## Retrieve a handle to the template element's contents.
+      ## Note: this function must never return nil.
+
+  DOMBuilderGetTagType*[Handle] =
+    proc(builder: DOMBuilder[Handle], handle: Handle): TagType {.nimcall.}
+      ## Retrieve the tag type of element.
+
+  DOMBuilderGetLocalName*[Handle] =
+    proc(builder: DOMBuilder[Handle], handle: Handle): string {.nimcall.}
+      ## Retrieve the local name of element. (This is tagName(getTagType),
+      ## unless the tag is unknown.
+
+  DOMBuilderGetNamespace*[Handle] =
+    proc(builder: DOMBuilder[Handle], handle: Handle): Namespace {.nimcall.}
+      ## Retrieve the namespace of element.
+
+  DOMBuilderCreateElement*[Handle] =
+    proc(builder: DOMBuilder[Handle], localName: string, namespace: Namespace,
+        tagType: TagType, attrs: Table[string, string]): Handle {.nimcall.}
+      ## Create a new element node.
+      ##
+      ## localName is the tag name of the token.
+      ##
+      ## namespace is the namespace passed to the function. (For HTML elements,
+      ## it's HTML.)
+      ## tagType is set based on localName. (This saves the consumer from
+      ## having to interpret localName again.)
+      ##
+      ## attrs is a table of the token's attributes.
+
+  DOMBuilderCreateComment*[Handle] =
+    proc(builder: DOMBuilder[Handle], text: string): Handle {.nimcall.}
+      ## Create a new comment node.
+
+  DOMBuilderInsertText*[Handle] =
+    proc(builder: DOMBuilder[Handle], parent: Handle, text: string,
+        before: Handle) {.nimcall.}
+      ## Insert a text node at the specified location with contents
+      ## `text`. If the specified location has a previous sibling that is
+      ## a text node, no new text node should be created, but instead `text`
+      ## should be appended to the previous sibling's character data.
+
+  DOMBuilderCreateDocumentType*[Handle] =
+    proc(builder: DOMBuilder[Handle], name, publicId, systemId: string): Handle
+        {.nimcall.}
+    ## Create a new document type node.
+
+  DOMBuilderInsertBefore*[Handle] =
+    proc(builder: DOMBuilder[Handle], parent, child, before: Handle)
+        {.nimcall.}
+      ## Insert node `child` before the node called `before`.
+      ##
+      ## If `before` is nil, `child` is expected to be appended to `parent`'s
+      ## node list.
+      ##
+      ## If `child` is a text, and its previous sibling after insertion is a
+      ## text as well, then they should be merged. `before` is never a
+      ## text node (and thus never has to be merged).
+      ##
+      ## Note: parent may either be an Element or a Document node.
+
+  DOMBuilderReparent*[Handle] =
+    proc(builder: DOMBuilder[Handle], child, newParent: Handle) {.nimcall.}
+      ## Remove `child` from its parent node, and append it to `newParent`.
+      ## In terms of DOM operations, this should be equivalent to calling
+      ## `child.remove()`, followed by `newParent.append(child)`.
+
+  DOMBuilderAddAttrsIfMissing*[Handle] =
+    proc(builder: DOMBuilder[Handle], element: Handle,
+        attrs: Table[string, string]) {.nimcall.}
+      ## Add the attributes in `attrs` to the element node `element`.
+      ## At the time of writing, called for HTML and BODY only. (This may
+      ## change in the future.)
+      ## An example implementation:
+      ## ```nim
+      ## for k, v in attrs:
+      ##   if k notin element.attrs:
+      ##     element.attrs[k] = v
+      ## ```
+
+  DOMBuilderSetScriptAlreadyStarted*[Handle] =
+    proc(builder: DOMBuilder[Handle], script: Handle) {.nimcall.}
+      ## Set the "already started" flag for the script element.
+      ##
+      ## Note: this flag is not togglable, so this callback should just set it
+      ## to true.
+
+  DOMBuilderAssociateWithForm*[Handle] =
+    proc(builder: DOMBuilder[Handle], element, form: Handle) {.nimcall.}
+      ## Called after createElement. Attempts to set form for form-associated
+      ## elements. Note: the DOM builder is responsible for checking whether
+      ## the two nodes are in the same tree.
+
+  DOMBuilderIsSVGIntegrationPoint*[Handle] =
+    proc(builder: DOMBuilder[Handle], element: Handle): bool {.nimcall.}
+      ## Check if element is an SVG integration point.
+
 type
   CharsetConfidence = enum
     CONFIDENCE_TENTATIVE, CONFIDENCE_CERTAIN, CONFIDENCE_IRRELEVANT
 
-  DOMParser = ref object # JS interface
-
-  OpenElements = seq[Element]
-
-  HTML5Parser = object
-    case fragment: bool
-    of true: ctx: Element
-    else: discard
+  HTML5Parser[Handle] = object
+    quirksMode: QuirksMode
+    dombuilder: DOMBuilder[Handle]
+    opts: HTML5ParserOpts[Handle]
+    fragment: bool
+    ctx: Handle
     needsreinterpret: bool
     charset: Charset
     confidence: CharsetConfidence
-    openElements: OpenElements
+    openElements: seq[Handle]
     insertionMode: InsertionMode
     oldInsertionMode: InsertionMode
     templateModes: seq[InsertionMode]
-    head: Element
+    head: Handle
     tokenizer: Tokenizer
-    document: Document
-    form: HTMLFormElement
+    form: Handle
     fosterParenting: bool
-    scripting: bool
-    activeFormatting: seq[(Element, Token)] # nil => marker
+    # Handle is an element. nil => marker
+    activeFormatting: seq[(Handle, Token)]
     framesetok: bool
     ignoreLF: bool
     pendingTableChars: string
     pendingTableCharsWhitespace: bool
 
-  AdjustedInsertionLocation = tuple[inside: Node, before: Node]
+  AdjustedInsertionLocation[Handle] = tuple[inside, before: Handle]
 
 # 13.2.4.1
   InsertionMode = enum
@@ -59,8 +267,111 @@ type
     AFTER_BODY, IN_FRAMESET, AFTER_FRAMESET, AFTER_AFTER_BODY,
     AFTER_AFTER_FRAMESET
 
-jsDestructor(DOMParser)
+# DOMBuilder interface functions
+proc finish[Handle](parser: HTML5Parser[Handle]) =
+  if parser.dombuilder.finish != nil:
+    parser.dombuilder.finish(parser.dombuilder)
+
+proc setQuirksMode[Handle](parser: var HTML5Parser[Handle], mode: QuirksMode) =
+  parser.quirksMode = mode
+  if parser.dombuilder.setQuirksMode != nil:
+    parser.dombuilder.setQuirksMode(parser.dombuilder, mode)
+
+func document[Handle](parser: HTML5Parser[Handle]): Handle {.inline.} =
+  return parser.dombuilder.document
+
+func getTemplateContent[Handle](parser: HTML5Parser[Handle],
+    handle: Handle): Handle =
+  return parser.dombuilder.getTemplateContent(parser.dombuilder, handle)
+
+func getLocalName[Handle](parser: HTML5Parser[Handle], handle: Handle):
+    string =
+  return parser.dombuilder.getLocalName(parser.dombuilder, handle)
+
+func getTagType[Handle](parser: HTML5Parser[Handle], handle: Handle): TagType =
+  if parser.dombuilder.getTagType != nil:
+    return parser.dombuilder.getTagType(parser.dombuilder, handle)
+  return tagType(parser.getLocalName(handle))
+
+func getNamespace[Handle](parser: HTML5Parser[Handle], handle: Handle):
+    Namespace =
+  if parser.dombuilder.getNamespace != nil:
+    return parser.dombuilder.getNamespace(parser.dombuilder, handle)
+  return Namespace.HTML
+
+func createElement[Handle](parser: HTML5Parser[Handle], localName: string,
+    namespace: Namespace, tagType: TagType, attrs: Table[string, string]):
+    Handle =
+  return parser.dombuilder.createElement(parser.dombuilder, localName,
+    namespace, tagType, attrs)
+
+func createElement[Handle](parser: HTML5Parser[Handle], tagType: TagType,
+    namespace: Namespace): Handle =
+  return parser.createElement(tagName(tagType), namespace, tagType,
+    Table[string, string]())
+
+func createComment[Handle](parser: HTML5Parser[Handle], text: string): Handle =
+  let dombuilder = parser.dombuilder
+  return dombuilder.createComment(dombuilder, text)
+
+proc createDocumentType[Handle](parser: HTML5Parser[Handle], name, publicId,
+    systemId: string): Handle =
+  let dombuilder = parser.dombuilder
+  return dombuilder.createDocumentType(dombuilder, name, publicId, systemId)
+
+proc insertBefore[Handle](parser: HTML5Parser[Handle],
+    parent, node, before: Handle) =
+  let dombuilder = parser.dombuilder
+  dombuilder.insertBefore(dombuilder, parent, node, before)
+
+proc insertText[Handle](parser: HTML5Parser[Handle], parent: Handle,
+    text: string, before: Handle) =
+  let dombuilder = parser.dombuilder
+  dombuilder.insertText(dombuilder, parent, text, before)
+
+proc reparent[Handle](parser: HTML5Parser[Handle], child, parent: Handle) =
+  let dombuilder = parser.dombuilder
+  dombuilder.reparent(dombuilder, child, parent)
+
+proc addAttrsIfMissing[Handle](parser: HTML5Parser, element: Handle,
+    attrs: Table[string, string]) =
+  let dombuilder = parser.dombuilder
+  if dombuilder.addAttrsIfMissing != nil:
+    dombuilder.addAttrsIfMissing(dombuilder, element, attrs)
+
+proc setScriptAlreadyStarted[Handle](parser: HTML5Parser, script: Handle) =
+  let dombuilder = parser.dombuilder
+  if dombuilder.setScriptAlreadyStarted != nil:
+    dombuilder.setScriptAlreadyStarted(dombuilder, script)
+
+proc associateWithForm[Handle](parser: HTML5Parser, element, form: Handle) =
+  let dombuilder = parser.dombuilder
+  if dombuilder.associateWithForm != nil:
+    dombuilder.associateWithForm(dombuilder, element, form)
+
+func isSVGIntegrationPoint[Handle](parser: HTML5Parser,
+    element: Handle): bool =
+  let dombuilder = parser.dombuilder
+  if dombuilder.isSVGIntegrationPoint != nil:
+    return dombuilder.isSVGIntegrationPoint(dombuilder, element)
+  return false
 
+# Parser
+func tagNameEquals[Handle](parser: HTML5Parser, handle: Handle,
+    token: Token): bool =
+  let tagType = parser.getTagType(handle)
+  if tagType != TAG_UNKNOWN:
+    return tagType == token.tagtype
+  let localName = parser.getLocalName(handle)
+  return localName == token.tagname
+
+func tagNameEquals[Handle](parser: HTML5Parser, a, b: Handle): bool =
+  let tagType = parser.getTagType(a)
+  if tagType != TAG_UNKNOWN:
+    return tagType == parser.getTagType(b)
+  return parser.getLocalName(a) == parser.getLocalName(b)
+
+# https://html.spec.whatwg.org/multipage/parsing.html#reset-the-insertion-mode-appropriately
 proc resetInsertionMode(parser: var HTML5Parser) =
   template switch_insertion_mode_and_return(mode: InsertionMode) =
     parser.insertionMode = mode
@@ -70,21 +381,23 @@ proc resetInsertionMode(parser: var HTML5Parser) =
     let last = i == 0
     if parser.fragment:
       node = parser.ctx
-    if node.tagType == TAG_SELECT:
+    let tagType = parser.getTagType(node)
+    if tagType == TAG_SELECT:
       if not last:
         for j in countdown(parser.openElements.high, 1):
           let ancestor = parser.openElements[j]
-          case ancestor.tagType
+          case parser.getTagType(ancestor)
           of TAG_TEMPLATE: break
           of TAG_TABLE: switch_insertion_mode_and_return IN_SELECT_IN_TABLE
           else: discard
       switch_insertion_mode_and_return IN_SELECT
-    case node.tagType
+    case tagType
     of TAG_TD, TAG_TH:
       if not last:
         switch_insertion_mode_and_return IN_CELL
     of TAG_TR: switch_insertion_mode_and_return IN_ROW
-    of TAG_TBODY, TAG_THEAD, TAG_TFOOT: switch_insertion_mode_and_return IN_CAPTION
+    of TAG_TBODY, TAG_THEAD, TAG_TFOOT:
+      switch_insertion_mode_and_return IN_CAPTION
     of TAG_COLGROUP: switch_insertion_mode_and_return IN_COLUMN_GROUP
     of TAG_TABLE: switch_insertion_mode_and_return IN_TABLE
     of TAG_TEMPLATE: switch_insertion_mode_and_return parser.templateModes[^1]
@@ -102,32 +415,38 @@ proc resetInsertionMode(parser: var HTML5Parser) =
     if last:
       switch_insertion_mode_and_return IN_BODY
 
-func currentNode(parser: HTML5Parser): Element =
+func currentNode[Handle](parser: HTML5Parser[Handle]): Handle =
   return parser.openElements[^1]
 
-func adjustedCurrentNode(parser: HTML5Parser): Element =
+func adjustedCurrentNode[Handle](parser: HTML5Parser[Handle]): Handle =
   if parser.fragment: parser.ctx
   else: parser.currentNode
 
 template parse_error() = discard
 
-func lastElementOfTag(parser: HTML5Parser, tagType: TagType): tuple[element: Element, pos: int] =
+func lastElementOfTag[Handle](parser: HTML5Parser[Handle],
+    tagType: TagType): tuple[element: Handle, pos: int] =
   for i in countdown(parser.openElements.high, 0):
-    if parser.openElements[i].tagType == tagType:
+    if parser.getTagType(parser.openElements[i]) == tagType:
       return (parser.openElements[i], i)
   return (nil, -1)
 
-template last_child_of(n: Node): AdjustedInsertionLocation =
+template last_child_of[Handle](n: Handle): AdjustedInsertionLocation[Handle] =
   (n, nil)
 
-# 13.2.6.1
-func appropriatePlaceForInsert(parser: HTML5Parser, target: Element): AdjustedInsertionLocation =
-  assert parser.openElements[0].tagType == TAG_HTML
-  if parser.fosterParenting and target.tagType in {TAG_TABLE, TAG_TBODY, TAG_TFOOT, TAG_THEAD, TAG_TR}:
+# https://html.spec.whatwg.org/multipage/#appropriate-place-for-inserting-a-node
+func appropriatePlaceForInsert[Handle](parser: HTML5Parser, target: Handle):
+    AdjustedInsertionLocation[Handle] =
+  assert parser.getTagType(parser.openElements[0]) == TAG_HTML
+  let targetTagType = parser.getTagType(target)
+  const FosterTagTypes = {TAG_TABLE, TAG_TBODY, TAG_TFOOT, TAG_THEAD, TAG_TR}
+  if parser.fosterParenting and targetTagType in FosterTagTypes:
     let lastTemplate = parser.lastElementOfTag(TAG_TEMPLATE)
     let lastTable = parser.lastElementOfTag(TAG_TABLE)
-    if lastTemplate.element != nil and (lastTable.element == nil or lastTable.pos < lastTemplate.pos):
-      return last_child_of(HTMLTemplateElement(lastTemplate.element).content)
+    if lastTemplate.element != nil and
+        (lastTable.element == nil or lastTable.pos < lastTemplate.pos):
+      let content = parser.getTemplateContent(lastTemplate.element)
+      return last_child_of(content)
     if lastTable.element == nil:
       return last_child_of(parser.openElements[0])
     if lastTable.element.parentNode != nil:
@@ -136,118 +455,142 @@ func appropriatePlaceForInsert(parser: HTML5Parser, target: Element): AdjustedIn
     result = last_child_of(previousElement)
   else:
     result = last_child_of(target)
-  if result.inside.nodeType == ELEMENT_NODE and Element(result.inside).tagType == TAG_TEMPLATE:
-    result = (HTMLTemplateElement(result.inside).content, nil)
+  if parser.getTagType(result.inside) == TAG_TEMPLATE:
+    result = (parser.getTemplateContent(result.inside), nil)
 
-func appropriatePlaceForInsert(parser: HTML5Parser): AdjustedInsertionLocation =
+func appropriatePlaceForInsert[Handle](parser: HTML5Parser[Handle]):
+    AdjustedInsertionLocation[Handle] =
   parser.appropriatePlaceForInsert(parser.currentNode)
 
-func hasElement(elements: seq[Element], tag: TagType): bool =
-  for element in elements:
-    if element.tagType == tag:
+func hasElement[Handle](parser: HTML5Parser[Handle], tag: TagType): bool =
+  for element in parser.openElements:
+    if parser.getTagType(element) == tag:
       return true
   return false
 
-func hasElementInSpecificScope(elements: seq[Element], target: Element, list: set[TagType]): bool =
-  for i in countdown(elements.high, 0):
-    if elements[i] == target:
+func hasElementInSpecificScope[Handle](parser: HTML5Parser[Handle],
+    target: Handle, list: set[TagType]): bool =
+  for i in countdown(parser.openElements.high, 0):
+    if parser.openElements[i] == target:
       return true
-    if elements[i].tagType in list:
+    if parser.getTagType(parser.openElements[i]) in list:
       return false
   assert false
 
-func hasElementInSpecificScope(elements: seq[Element], target: TagType, list: set[TagType]): bool =
-  for i in countdown(elements.high, 0):
-    if elements[i].tagType == target:
+func hasElementInSpecificScope[Handle](parser: HTML5Parser[Handle],
+    target: TagType, list: set[TagType]): bool =
+  for i in countdown(parser.openElements.high, 0):
+    let tagType = parser.getTagType(parser.openElements[i])
+    if tagType == target:
       return true
-    if elements[i].tagType in list:
+    if tagType in list:
       return false
   assert false
 
-func hasElementInSpecificScope(elements: seq[Element], target: set[TagType], list: set[TagType]): bool =
-  for i in countdown(elements.high, 0):
-    if elements[i].tagType in target:
+func hasElementInSpecificScope[Handle](parser: HTML5Parser[Handle],
+    target: set[TagType], list: set[TagType]): bool =
+  for i in countdown(parser.openElements.high, 0):
+    let tagType = parser.getTagType(parser.openElements[i])
+    if tagType in target:
       return true
-    if elements[i].tagType in list:
+    if tagType in list:
       return false
   assert false
 
-const Scope = {TAG_APPLET, TAG_CAPTION, TAG_HTML, TAG_TABLE, TAG_TD, TAG_TH,
-               TAG_MARQUEE, TAG_OBJECT, TAG_TEMPLATE} #TODO SVG (NOTE MathML not implemented)
-func hasElementInScope(elements: seq[Element], target: TagType): bool =
-  return elements.hasElementInSpecificScope(target, Scope)
-
-func hasElementInScope(elements: seq[Element], target: set[TagType]): bool =
-  return elements.hasElementInSpecificScope(target, Scope)
-
-func hasElementInScope(elements: seq[Element], target: Element): bool =
-  return elements.hasElementInSpecificScope(target, Scope)
-
-func hasElementInListItemScope(elements: seq[Element], target: TagType): bool =
-  return elements.hasElementInSpecificScope(target, Scope + {TAG_OL, TAG_UL})
-
-func hasElementInButtonScope(elements: seq[Element], target: TagType): bool =
-  return elements.hasElementInSpecificScope(target, Scope + {TAG_BUTTON})
-
-func hasElementInTableScope(elements: seq[Element], target: TagType): bool =
-  return elements.hasElementInSpecificScope(target, {TAG_HTML, TAG_TABLE, TAG_TEMPLATE})
-
-func hasElementInTableScope(elements: seq[Element], target: set[TagType]): bool =
-  return elements.hasElementInSpecificScope(target, {TAG_HTML, TAG_TABLE, TAG_TEMPLATE})
-
-func hasElementInSelectScope(elements: seq[Element], target: TagType): bool =
-  for i in countdown(elements.high, 0):
-    if elements[i].tagType == target:
+const Scope = {
+  TAG_APPLET, TAG_CAPTION, TAG_HTML, TAG_TABLE, TAG_TD, TAG_TH, TAG_MARQUEE,
+  TAG_OBJECT, TAG_TEMPLATE #TODO SVG
+  # Note: MathML is not implemented
+}
+
+func hasElementInScope[Handle](parser: HTML5Parser[Handle],
+    target: TagType): bool =
+  return parser.hasElementInSpecificScope(target, Scope)
+
+func hasElementInScope[Handle](parser: HTML5Parser[Handle],
+    target: set[TagType]): bool =
+  return parser.hasElementInSpecificScope(target, Scope)
+
+func hasElementInScope[Handle](parser: HTML5Parser[Handle],
+    target: Handle): bool =
+  return parser.hasElementInSpecificScope(target, Scope)
+
+func hasElementInListItemScope[Handle](parser: HTML5Parser[Handle],
+    target: TagType): bool =
+  const ListItemScope = Scope + {TAG_OL, TAG_UL}
+  return parser.hasElementInSpecificScope(target, ListItemScope)
+
+func hasElementInButtonScope[Handle](parser: HTML5Parser[Handle],
+    target: TagType): bool =
+  const ButtonScope = Scope + {TAG_BUTTON}
+  return parser.hasElementInSpecificScope(target, ButtonScope)
+
+const TableScope = {TAG_HTML, TAG_TABLE, TAG_TEMPLATE}
+func hasElementInTableScope[Handle](parser: HTML5Parser[Handle],
+    target: TagType): bool =
+  return parser.hasElementInSpecificScope(target, TableScope)
+
+func hasElementInTableScope[Handle](parser: HTML5Parser[Handle],
+    target: set[TagType]): bool =
+  return parser.hasElementInSpecificScope(target, TableScope)
+
+func hasElementInSelectScope[Handle](parser: HTML5Parser[Handle],
+    target: TagType): bool =
+  for i in countdown(parser.openElements.high, 0):
+    let tagType = parser.getTagType(parser.openElements[i])
+    if tagType == target:
       return true
-    if elements[i].tagType notin {TAG_OPTION, TAG_OPTGROUP}:
+    if tagType notin {TAG_OPTION, TAG_OPTGROUP}:
       return false
   assert false
 
-func createElement(parser: HTML5Parser, token: Token, namespace: Namespace, intendedParent: Node): Element =
+func createElement[Handle](parser: HTML5Parser[Handle], token: Token,
+    namespace: Namespace, intendedParent: Handle): Handle =
   #TODO custom elements
-  let document = intendedParent.document
   let localName = token.tagname
-  let element = document.newHTMLElement(localName, namespace, tagType = token.tagtype, attrs = token.attrs)
-  if element.isResettable():
-    element.resetElement()
-
-  if element.tagType in SupportedFormAssociatedElements and parser.form != nil and
-      not parser.openElements.hasElement(TAG_TEMPLATE) and
-      (element.tagType notin ListedElements or not element.attrb("form")) and
-      intendedParent.inSameTree(parser.form):
-    let element = FormAssociatedElement(element)
-    element.setForm(parser.form)
-    element.parserInserted = true
+  let element = parser.createElement(localName, namespace, token.tagtype,
+    token.attrs)
+  if token.tagtype in FormAssociatedElements and parser.form != nil and
+      not parser.hasElement(TAG_TEMPLATE) and
+      (token.tagtype notin ListedElements or "form" notin token.attrs):
+    parser.associateWithForm(element, parser.form)
   return element
 
-proc pushElement(parser: var HTML5Parser, node: Element) =
+proc pushElement[Handle](parser: var HTML5Parser, node: Handle) =
   parser.openElements.add(node)
-  parser.tokenizer.hasnonhtml = not parser.adjustedCurrentNode().inHTMLNamespace()
+  let node = parser.adjustedCurrentNode()
+  parser.tokenizer.hasnonhtml = parser.getNamespace(node) != Namespace.HTML
 
-proc popElement(parser: var HTML5Parser): Element =
+proc popElement[Handle](parser: var HTML5Parser[Handle]): Handle =
   result = parser.openElements.pop()
-  if result.tagType == TAG_TEXTAREA:
-    result.resetElement()
+  if parser.dombuilder.elementPopped != nil:
+    parser.dombuilder.elementPopped(parser.dombuilder, result)
   if parser.openElements.len == 0:
     parser.tokenizer.hasnonhtml = false
   else:
-    parser.tokenizer.hasnonhtml = not parser.adjustedCurrentNode().inHTMLNamespace()
+    let node = parser.adjustedCurrentNode()
+    parser.tokenizer.hasnonhtml = parser.getNamespace(node) != Namespace.HTML
 
 template pop_current_node = discard parser.popElement()
 
-proc insert(location: AdjustedInsertionLocation, node: Node) =
-  location.inside.insert(node, location.before)
+proc insert[Handle](parser: HTML5Parser[Handle],
+    location: AdjustedInsertionLocation[Handle], node: Handle) =
+  parser.insertBefore(location.inside, node, location.before)
+
+proc append[Handle](parser: HTML5Parser[Handle], parent, node: Handle) =
+  parser.insertBefore(parent, node, nil)
 
-proc insertForeignElement(parser: var HTML5Parser, token: Token, namespace: Namespace): Element =
+proc insertForeignElement[Handle](parser: var HTML5Parser[Handle], token: Token,
+    namespace: Namespace): Handle =
   let location = parser.appropriatePlaceForInsert()
   let element = parser.createElement(token, namespace, location.inside)
-  if location.inside.preInsertionValidity(element, location.before).isOk:
-    #TODO custom elements
-    location.insert(element)
+  #TODO custom elements
+  parser.insert(location, element)
   parser.pushElement(element)
   return element
 
-proc insertHTMLElement(parser: var HTML5Parser, token: Token): Element =
+proc insertHTMLElement[Handle](parser: var HTML5Parser[Handle],
+    token: Token): Handle =
   return parser.insertForeignElement(token, Namespace.HTML)
 
 proc adjustSVGAttributes(token: Token) =
@@ -322,15 +665,7 @@ template insert_character_impl(parser: var HTML5Parser, data: typed) =
   let location = parser.appropriatePlaceForInsert()
   if location.inside.nodeType == DOCUMENT_NODE:
     return
-  let insertNode = if location.before == nil:
-    location.inside.lastChild
-  else:
-    location.before.previousSibling
-  if insertNode != nil and insertNode.nodeType == TEXT_NODE:
-    dom.Text(insertNode).data &= data
-  else:
-    let text = location.inside.document.createTextNode($data)
-    location.insert(text)
+  insertText(parser, location.inside, $data, location.before)
 
 proc insertCharacter(parser: var HTML5Parser, data: string) =
   insert_character_impl(parser, data)
@@ -341,17 +676,19 @@ proc insertCharacter(parser: var HTML5Parser, data: char) =
 proc insertCharacter(parser: var HTML5Parser, data: Rune) =
   insert_character_impl(parser, data)
 
-proc insertComment(parser: var HTML5Parser, token: Token, position: AdjustedInsertionLocation) =
-  position.insert(position.inside.document.createComment(token.data))
+proc insertComment[Handle](parser: var HTML5Parser[Handle], token: Token,
+    position: AdjustedInsertionLocation[Handle]) =
+  let comment = parser.createComment(token.data)
+  parser.insert(position, comment)
 
 proc insertComment(parser: var HTML5Parser, token: Token) =
   let position = parser.appropriatePlaceForInsert()
-  position.insert(position.inside.document.createComment(token.data))
+  parser.insertComment(token, position)
 
 const PublicIdentifierEquals = [
   "-//W3O//DTD W3 HTML Strict 3.0//EN//",
   "-/W3C/DTD HTML 4.0 Transitional/EN",
-  "HTML" 
+  "HTML"
 ]
 
 const PublicIdentifierStartsWith = [
@@ -409,7 +746,7 @@ const PublicIdentifierStartsWith = [
   "-//W3C//DTD W3 HTML//",
   "-//W3O//DTD W3 HTML 3.0//",
   "-//WebTechs//DTD Mozilla HTML 2.0//",
-  "-//WebTechs//DTD Mozilla HTML//", 
+  "-//WebTechs//DTD Mozilla HTML//",
 ]
 
 const SystemIdentifierMissingAndPublicIdentifierStartsWith = [
@@ -469,17 +806,28 @@ proc genericRCDATAElementParsingAlgorithm(parser: var HTML5Parser, token: Token)
   parser.oldInsertionMode = parser.insertionMode
   parser.insertionMode = TEXT
 
-# 13.2.6.3
+# Pop all elements, including the specified tag.
+proc popElementsIncl(parser: var HTML5Parser, tag: TagType) =
+  while parser.getTagType(parser.popElement()) != tag:
+    discard
+
+proc popElementsIncl(parser: var HTML5Parser, tags: set[TagType]) =
+  while parser.getTagType(parser.popElement()) notin tags:
+    discard
+
+# https://html.spec.whatwg.org/multipage/parsing.html#closing-elements-that-have-implied-end-tags
 proc generateImpliedEndTags(parser: var HTML5Parser) =
   const tags = {TAG_DD, TAG_DT, TAG_LI, TAG_OPTGROUP, TAG_OPTION, TAG_P,
                 TAG_RB, TAG_RP, TAG_RT, TAG_RTC}
-  while parser.currentNode.tagType in tags:
+  while parser.getTagType(parser.currentNode) in tags:
     discard parser.popElement()
 
 proc generateImpliedEndTags(parser: var HTML5Parser, exclude: TagType) =
-  let tags = {TAG_DD, TAG_DT, TAG_LI, TAG_OPTGROUP, TAG_OPTION, TAG_P,
-                TAG_RB, TAG_RP, TAG_RT, TAG_RTC} - {exclude}
-  while parser.currentNode.tagType in tags:
+  let tags = {
+    TAG_DD, TAG_DT, TAG_LI, TAG_OPTGROUP, TAG_OPTION, TAG_P, TAG_RB, TAG_RP,
+    TAG_RT, TAG_RTC
+  } - {exclude}
+  while parser.getTagType(parser.currentNode) in tags:
     discard parser.popElement()
 
 proc generateImpliedEndTagsThoroughly(parser: var HTML5Parser) =
@@ -487,30 +835,31 @@ proc generateImpliedEndTagsThoroughly(parser: var HTML5Parser) =
                 TAG_OPTGROUP, TAG_OPTION, TAG_P, TAG_RB, TAG_RP, TAG_RT,
                 TAG_RTC, TAG_TBODY, TAG_TD, TAG_TFOOT, TAG_TH, TAG_THEAD,
                 TAG_TR}
-  while parser.currentNode.tagType in tags:
+  while parser.getTagType(parser.currentNode) in tags:
     discard parser.popElement()
 
-# 13.2.4.3
-proc pushOntoActiveFormatting(parser: var HTML5Parser, element: Element, token: Token) =
+# https://html.spec.whatwg.org/multipage/parsing.html#push-onto-the-list-of-active-formatting-elements
+proc pushOntoActiveFormatting[Handle](parser: var HTML5Parser[Handle],
+    element: Handle, token: Token) =
   var count = 0
   for i in countdown(parser.activeFormatting.high, 0):
     let it = parser.activeFormatting[i]
     if it[0] == nil: break
-    if it[0].tagType != element.tagType: continue
-    if it[0].tagType == TAG_UNKNOWN:
-      if it[0].localName != element.localName: continue
-    if it[0].namespace != element.namespace: continue
+    if not parser.tagNameEquals(it[0], element):
+      continue
+    if parser.getNamespace(it[0]) != parser.getNamespace(element):
+      continue
     var fail = false
-    for k, v in it[0].attrs:
-      if k notin element.attrs:
+    for k, v in it[1].attrs:
+      if k notin token.attrs:
         fail = true
         break
-      if v != element.attrs[k]:
+      if v != token.attrs[k]:
         fail = true
         break
     if fail: continue
-    for k, v in element.attrs:
-      if k notin it[0].attrs:
+    for k, v in token.attrs:
+      if k notin it[1].attrs:
         fail = true
         break
     if fail: continue
@@ -520,15 +869,18 @@ proc pushOntoActiveFormatting(parser: var HTML5Parser, element: Element, token:
       break
   parser.activeFormatting.add((element, token))
 
-proc reconstructActiveFormatting(parser: var HTML5Parser) =
+proc reconstructActiveFormatting[Handle](parser: var HTML5Parser[Handle]) =
   type State = enum
     REWIND, ADVANCE, CREATE
   if parser.activeFormatting.len == 0:
     return
-  if parser.activeFormatting[^1][0] == nil or parser.openElements.hasElement(parser.activeFormatting[^1][0].tagType):
+  if parser.activeFormatting[^1][0] == nil:
+    return
+  let tagType = parser.getTagType(parser.activeFormatting[^1][0])
+  if parser.hasElement(tagType):
     return
   var i = parser.activeFormatting.high
-  template entry: Element = (parser.activeFormatting[i][0])
+  template entry: Handle = (parser.activeFormatting[i][0])
   var state = REWIND
   while true:
     {.computedGoto.}
@@ -538,8 +890,10 @@ proc reconstructActiveFormatting(parser: var HTML5Parser) =
         state = CREATE
         continue
       dec i
-      if entry != nil and not parser.openElements.hasElement(entry.tagType):
-        continue
+      if entry != nil:
+        let tagType = parser.getTagType(entry)
+        if not parser.hasElement(tagType):
+          continue
       state = ADVANCE
     of ADVANCE:
       inc i
@@ -554,8 +908,9 @@ proc reconstructActiveFormatting(parser: var HTML5Parser) =
 proc clearActiveFormattingTillMarker(parser: var HTML5Parser) =
   while parser.activeFormatting.len > 0 and parser.activeFormatting.pop()[0] != nil: discard
 
-func isHTMLIntegrationPoint(node: Element): bool =
-  return false #TODO SVG (NOTE MathML not implemented)
+func isHTMLIntegrationPoint[Handle](parser: HTML5Parser[Handle],
+    element: Handle): bool =
+  return parser.isSVGIntegrationPoint(element) # (NOTE MathML not implemented)
 
 func extractEncFromMeta(s: string): Charset =
   var i = 0
@@ -605,6 +960,116 @@ proc changeEncoding(parser: var HTML5Parser, cs: Charset) =
     parser.charset = cs
   parser.needsreinterpret = true
 
+proc adoptionAgencyAlgorithm[Handle](parser: var HTML5Parser[Handle],
+    token: Token): bool =
+  if parser.tagNameEquals(parser.currentNode, token):
+    var fail = true
+    for it in parser.activeFormatting:
+      if it[0] == parser.currentNode:
+        fail = false
+    if fail:
+      pop_current_node
+      return false
+  var i = 0
+  while true:
+    if i >= 8: return false
+    inc i
+    if parser.activeFormatting.len == 0: return true
+    var formatting: Handle
+    var formattingIndex: int
+    for j in countdown(parser.activeFormatting.high, 0):
+      let element = parser.activeFormatting[j][0]
+      if element == nil:
+        return true
+      if parser.tagNameEquals(parser.currentNode, token):
+        formatting = element
+        formattingIndex = j
+        break
+      if j == 0:
+        return true
+    let stackIndex = parser.openElements.find(formatting)
+    if stackIndex < 0:
+      parse_error
+      parser.activeFormatting.delete(formattingIndex)
+      return false
+    if not parser.hasElementInScope(formatting):
+      parse_error
+      return false
+    if formatting != parser.currentNode: parse_error
+    var furthestBlock: Handle = nil
+    var furthestBlockIndex: int
+    for j in countdown(parser.openElements.high, 0):
+      if parser.openElements[j] == formatting:
+        break
+      if parser.getTagType(parser.openElements[j]) in SpecialElements:
+        furthestBlock = parser.openElements[j]
+        furthestBlockIndex = j
+        break
+    if furthestBlock == nil:
+      while parser.popElement() != formatting: discard
+      parser.activeFormatting.delete(formattingIndex)
+      return false
+    let commonAncestor = parser.openElements[stackIndex - 1]
+    var bookmark = formattingIndex
+    var node = furthestBlock
+    var aboveNode = parser.openElements[furthestBlockIndex - 1]
+    var lastNode = furthestBlock
+    var j = 0
+    while true:
+      inc j
+      node = aboveNode
+      let nodeStackIndex = parser.openElements.find(node)
+      if node == formatting: break
+      var nodeFormattingIndex = -1
+      for i in countdown(parser.activeFormatting.high, 0):
+        if parser.activeFormatting[i][0] == node:
+          nodeFormattingIndex = i
+          break
+      if j > 3 and nodeFormattingIndex >= 0:
+        parser.activeFormatting.delete(nodeFormattingIndex)
+        if nodeFormattingIndex < bookmark:
+          dec bookmark # a previous node got deleted, so decrease bookmark by one
+      if nodeFormattingIndex < 0:
+        aboveNode = parser.openElements[nodeStackIndex - 1]
+        parser.openElements.delete(nodeStackIndex)
+        if nodeStackIndex < furthestBlockIndex:
+          dec furthestBlockIndex
+          furthestBlock = parser.openElements[furthestBlockIndex]
+        continue
+      let element = parser.createElement(parser.activeFormatting[nodeFormattingIndex][1], Namespace.HTML, commonAncestor)
+      parser.activeFormatting[nodeFormattingIndex] = (element, parser.activeFormatting[nodeFormattingIndex][1])
+      parser.openElements[nodeStackIndex] = element
+      aboveNode = parser.openElements[nodeStackIndex - 1]
+      node = element
+      if lastNode == furthestBlock:
+        bookmark = nodeFormattingIndex + 1
+      parser.append(node, lastNode)
+      lastNode = node
+    let location = parser.appropriatePlaceForInsert(commonAncestor)
+    location.inside.insert(lastNode, location.before)
+    let token = parser.activeFormatting[formattingIndex][1]
+    let element = parser.createElement(token, Namespace.HTML, furthestBlock)
+    var tomove: seq[Handle]
+    j = furthestBlock.childList.high
+    while j >= 0:
+      let child = furthestBlock.childList[j]
+      tomove.add(child)
+      dec j
+    for child in tomove:
+      parser.reparent(child, element)
+    parser.append(furthestBlock, element)
+    parser.activeFormatting.insert((element, token), bookmark)
+    parser.activeFormatting.delete(formattingIndex)
+    parser.openElements.insert(element, furthestBlockIndex)
+    parser.openElements.delete(stackIndex)
+
+proc closeP(parser: var HTML5Parser) =
+  parser.generateImpliedEndTags(TAG_P)
+  if parser.getTagType(parser.currentNode) != TAG_P:
+    parse_error
+  while parser.getTagType(parser.popElement()) != TAG_P:
+    discard
+
 # Following is an implementation of the state (?) machine defined in
 # https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inhtml
 # It uses the ad-hoc pattern matching macro `match' to apply the following
@@ -699,7 +1164,7 @@ macro match(token: Token, body: typed): untyped =
         of nnkCharLit:
           ofBranches[CHARACTER_ASCII].ofBranches.add((@[pattern], action))
           ofBranches[CHARACTER_ASCII].painted = true
-        else: error fmt"Unsupported curly of kind {pattern[0].kind}"
+        else: error "Unsupported curly of kind " & $pattern[0].kind
       of nnkStrLit:
         var tempTokenizer = newTokenizer(pattern.strVal)
         for token in tempTokenizer.tokenize:
@@ -716,14 +1181,17 @@ macro match(token: Token, body: typed): untyped =
             if not found:
               ofBranches[token.t].ofBranches.add((@[(quote do: TagType(`tt`))], action))
               ofBranches[token.t].painted = true
-          else: error fmt"{pattern.strVal}: Unsupported token {token} of kind {token.t}"
+          else:
+            error pattern.strVal & ": Unsupported token " & $token &
+              " of kind " & $token.t
           break
       of nnkDiscardStmt:
         defaultBranch = action
       of nnkTupleConstr:
         for child in pattern:
           patterns.add(child)
-      else: error fmt"{pattern}: Unsupported pattern of kind {pattern.kind}"
+      else:
+        error pattern.strVal & ": Unsupported pattern of kind " & $pattern.kind
 
   func tokenBranchOn(tok: TokenType): NimNode =
     case tok
@@ -733,7 +1201,8 @@ macro match(token: Token, body: typed): untyped =
       return quote do: token.r
     of CHARACTER_ASCII:
       return quote do: token.c
-    else: error fmt"Unsupported branching of token {tok}"
+    else:
+      error "Unsupported branching of token " & $tok
 
   template add_to_case(branch: typed) =
     if branch[0].len == 1:
@@ -781,7 +1250,8 @@ macro match(token: Token, body: typed): untyped =
     stmts.add(stmt)
   result = newBlockStmt(ident("inside_not_else"), stmts)
 
-proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = parser.insertionMode) =
+proc processInHTMLContent[Handle](parser: var HTML5Parser[Handle],
+    token: Token, insertionMode: InsertionMode) =
   template pop_all_nodes =
     while parser.openElements.len > 1: pop_current_node
   template anything_else = discard "anything_else"
@@ -790,7 +1260,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       discard (`v`, proc() = `body`)
   template _ = discard
   template reprocess(tok: Token) =
-    parser.processInHTMLContent(tok)
+    parser.processInHTMLContent(tok, parser.insertionMode)
 
   case insertionMode
   of INITIAL:
@@ -800,20 +1270,20 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       TokenType.DOCTYPE => (block:
         if token.name.isnone or token.name.get != "html" or token.pubid.issome or (token.sysid.issome and token.sysid.get != "about:legacy-compat"):
           parse_error
-        let doctype = parser.document.newDocumentType(token.name.get(""), token.pubid.get(""), token.sysid.get(""))
-        parser.document.append(doctype)
-        if not parser.document.is_iframe_srcdoc and not parser.document.parser_cannot_change_the_mode_flag:
+        let doctype = parser.createDocumentType(token.name.get(""),
+          token.pubid.get(""), token.sysid.get(""))
+        parser.append(parser.document, doctype)
+        if not parser.opts.isIframeSrcdoc:
           if quirksConditions(token):
-            parser.document.mode = QUIRKS
+            parser.setQuirksMode(QUIRKS)
           elif limitedQuirksConditions(token):
-            parser.document.mode = LIMITED_QUIRKS
+            parser.setQuirksMode(LIMITED_QUIRKS)
         parser.insertionMode = BEFORE_HTML
       )
       _ => (block:
-        if not parser.document.is_iframe_srcdoc:
+        if not parser.opts.isIframeSrcdoc:
           parse_error
-        if not parser.document.parser_cannot_change_the_mode_flag:
-          parser.document.mode = QUIRKS
+        parser.setQuirksMode(QUIRKS)
         parser.insertionMode = BEFORE_HTML
         reprocess token
       )
@@ -825,15 +1295,15 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       AsciiWhitespace => (block: discard)
       "<html>" => (block:
         let element = parser.createElement(token, Namespace.HTML, parser.document)
-        parser.document.append(element)
+        parser.append(parser.document, element)
         parser.pushElement(element)
         parser.insertionMode = BEFORE_HEAD
       )
       ("</head>", "</body>", "</html>", "</br>") => (block: anything_else)
       TokenType.END_TAG => (block: parse_error)
       _ => (block:
-        let element = parser.document.newHTMLElement(TAG_HTML, Namespace.HTML)
-        parser.document.append(element)
+        let element = parser.createElement(TAG_HTML, Namespace.HTML)
+        parser.append(parser.document, element)
         parser.pushElement(element)
         parser.insertionMode = BEFORE_HEAD
         reprocess token
@@ -868,20 +1338,22 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         pop_current_node
       )
       "<meta>" => (block:
-        let element = parser.insertHTMLElement(token)
+        discard parser.insertHTMLElement(token)
         pop_current_node
         if parser.confidence == CONFIDENCE_TENTATIVE:
-          let cs = getCharset(element.attr("charset"))
+          let cs = getCharset(token.attrs.getOrDefault("charset", ""))
           if cs != CHARSET_UNKNOWN:
             parser.changeEncoding(cs)
-          elif element.attr("http-equiv").equalsIgnoreCase("Content-Type"):
-            let cs = extractEncFromMeta(element.attr("content"))
-            if cs != CHARSET_UNKNOWN:
-              parser.changeEncoding(cs)
+          elif "http-equiv" in token.attrs:
+            if token.attrs["http-equiv"].equalsIgnoreCase("Content-Type") and
+                "content" in token.attrs:
+              let cs = extractEncFromMeta(token.attrs["content"])
+              if cs != CHARSET_UNKNOWN:
+                parser.changeEncoding(cs)
       )
       "<title>" => (block: parser.genericRCDATAElementParsingAlgorithm(token))
       "<noscript>" => (block:
-        if not parser.scripting:
+        if not parser.opts.scripting:
           discard parser.insertHTMLElement(token)
           parser.insertionMode = IN_HEAD_NOSCRIPT
         else:
@@ -890,13 +1362,9 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       ("<noframes>", "<style>") => (block: parser.genericRawtextElementParsingAlgorithm(token))
       "<script>" => (block:
         let location = parser.appropriatePlaceForInsert()
-        let element = HTMLScriptElement(parser.createElement(token, Namespace.HTML, location.inside))
-        element.parserDocument = parser.document
-        element.forceAsync = false
-        if parser.fragment:
-          element.alreadyStarted = true
+        let element = parser.createElement(token, Namespace.HTML, location.inside)
         #TODO document.write (?)
-        location.insert(element)
+        parser.insert(location, element)
         parser.pushElement(element)
         parser.tokenizer.state = SCRIPT_DATA
         parser.oldInsertionMode = parser.insertionMode
@@ -915,13 +1383,13 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         parser.templateModes.add(IN_TEMPLATE)
       )
       "</template>" => (block:
-        if not parser.openElements.hasElement(TAG_TEMPLATE):
+        if not parser.hasElement(TAG_TEMPLATE):
           parse_error
         else:
           parser.generateImpliedEndTagsThoroughly()
-          if parser.currentNode.tagType != TAG_TEMPLATE:
+          if parser.getTagType(parser.currentNode) != TAG_TEMPLATE:
             parse_error
-          while parser.popElement().tagType != TAG_TEMPLATE: discard
+          parser.popElementsIncl(TAG_TEMPLATE)
           parser.clearActiveFormattingTillMarker()
           discard parser.templateModes.pop()
           parser.resetInsertionMode()
@@ -987,114 +1455,6 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       )
 
   of IN_BODY:
-    proc closeP(parser: var HTML5Parser) =
-      parser.generateImpliedEndTags(TAG_P)
-      if parser.currentNode.tagType != TAG_P: parse_error
-      while parser.popElement().tagType != TAG_P: discard
-
-    proc adoptionAgencyAlgorithm(parser: var HTML5Parser, token: Token): bool =
-      if parser.currentNode.tagType != TAG_UNKNOWN and parser.currentNode.tagtype == token.tagtype or parser.currentNode.localName == token.tagname:
-        var fail = true
-        for it in parser.activeFormatting:
-          if it[0] == parser.currentNode:
-            fail = false
-        if fail:
-          pop_current_node
-          return false
-      var i = 0
-      while true:
-        if i >= 8: return false
-        inc i
-        if parser.activeFormatting.len == 0: return true
-        var formatting: Element
-        var formattingIndex: int
-        for j in countdown(parser.activeFormatting.high, 0):
-          let element = parser.activeFormatting[j][0]
-          if element == nil:
-            return true
-          if element.tagType != TAG_UNKNOWN and element.tagtype == token.tagtype or element.qualifiedName == token.tagname:
-            formatting = element
-            formattingIndex = j
-            break
-          if j == 0:
-            return true
-        let stackIndex = parser.openElements.find(formatting)
-        if stackIndex < 0:
-          parse_error
-          parser.activeFormatting.delete(formattingIndex)
-          return false
-        if not parser.openElements.hasElementInScope(formatting):
-          parse_error
-          return false
-        if formatting != parser.currentNode: parse_error
-        var furthestBlock: Element = nil
-        var furthestBlockIndex: int
-        for j in countdown(parser.openElements.high, 0):
-          if parser.openElements[j] == formatting:
-            break
-          if parser.openElements[j].tagType in SpecialElements:
-            furthestBlock = parser.openElements[j]
-            furthestBlockIndex = j
-            break
-        if furthestBlock == nil:
-          while parser.popElement() != formatting: discard
-          parser.activeFormatting.delete(formattingIndex)
-          return false
-        let commonAncestor = parser.openElements[stackIndex - 1]
-        var bookmark = formattingIndex
-        var node = furthestBlock
-        var aboveNode = parser.openElements[furthestBlockIndex - 1]
-        var lastNode = furthestBlock
-        var j = 0
-        while true:
-          inc j
-          node = aboveNode
-          let nodeStackIndex = parser.openElements.find(node)
-          if node == formatting: break
-          var nodeFormattingIndex = -1
-          for i in countdown(parser.activeFormatting.high, 0):
-            if parser.activeFormatting[i][0] == node:
-              nodeFormattingIndex = i
-              break
-          if j > 3 and nodeFormattingIndex >= 0:
-            parser.activeFormatting.delete(nodeFormattingIndex)
-            if nodeFormattingIndex < bookmark:
-              dec bookmark # a previous node got deleted, so decrease bookmark by one
-          if nodeFormattingIndex < 0:
-            aboveNode = parser.openElements[nodeStackIndex - 1]
-            parser.openElements.delete(nodeStackIndex)
-            if nodeStackIndex < furthestBlockIndex:
-              dec furthestBlockIndex
-              furthestBlock = parser.openElements[furthestBlockIndex]
-            continue
-          let element = parser.createElement(parser.activeFormatting[nodeFormattingIndex][1], Namespace.HTML, commonAncestor)
-          parser.activeFormatting[nodeFormattingIndex] = (element, parser.activeFormatting[nodeFormattingIndex][1])
-          parser.openElements[nodeStackIndex] = element
-          aboveNode = parser.openElements[nodeStackIndex - 1]
-          node = element
-          if lastNode == furthestBlock:
-            bookmark = nodeFormattingIndex + 1
-          node.append(lastNode)
-          lastNode = node
-        let location = parser.appropriatePlaceForInsert(commonAncestor)
-        location.inside.insert(lastNode, location.before)
-        let token = parser.activeFormatting[formattingIndex][1]
-        let element = parser.createElement(token, Namespace.HTML, furthestBlock)
-        var tomove: seq[Node]
-        j = furthestBlock.childList.high
-        while j >= 0:
-          let child = furthestBlock.childList[j]
-          child.remove(true)
-          tomove.add(child)
-          dec j
-        for child in tomove:
-          element.append(child)
-        furthestBlock.append(element)
-        parser.activeFormatting.insert((element, token), bookmark)
-        parser.activeFormatting.delete(formattingIndex)
-        parser.openElements.insert(element, furthestBlockIndex)
-        parser.openElements.delete(stackIndex)
-
     template any_other_start_tag() =
       parser.reconstructActiveFormatting()
       discard parser.insertHTMLElement(token)
@@ -1102,15 +1462,15 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
     template any_other_end_tag() =
       for i in countdown(parser.openElements.high, 0):
         let node = parser.openElements[i]
-        if node.tagType != TAG_UNKNOWN and node.tagType == token.tagtype or node.localName == token.tagname:
+        if parser.tagNameEquals(node, token):
           parser.generateImpliedEndTags(token.tagtype)
           if node != parser.currentNode: parse_error
           while parser.popElement() != node: discard
           break
-        elif node.tagType in SpecialElements:
+        elif parser.getTagType(node) in SpecialElements:
           parse_error
           return
-    
+
     match token:
       '\0' => (block: parse_error)
       AsciiWhitespace => (block:
@@ -1131,28 +1491,28 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       TokenType.DOCTYPE => (block: parse_error)
       "<html>" => (block:
         parse_error
-        if parser.openElements.hasElement(TAG_TEMPLATE):
+        if parser.hasElement(TAG_TEMPLATE):
           discard
         else:
-          for k, v in token.attrs:
-            if k notin parser.openElements[0].attrs:
-              parser.openElements[0].attr(k, v)
+          parser.addAttrsIfMissing(parser.openElements[0], token.attrs)
       )
       ("<base>", "<basefont>", "<bgsound>", "<link>", "<meta>", "<noframes>", "<script>", "<style>", "<template>", "<title>",
        "</template>") => (block: parser.processInHTMLContent(token, IN_HEAD))
       "<body>" => (block:
         parse_error
-        if parser.openElements.len == 1 or parser.openElements[1].tagType != TAG_BODY or parser.openElements.hasElement(TAG_TEMPLATE):
+        if parser.openElements.len == 1 or
+            parser.getTagType(parser.openElements[1]) != TAG_BODY or
+            parser.hasElement(TAG_TEMPLATE):
           discard
         else:
           parser.framesetOk = false
-          for k, v in token.attrs:
-            if k notin parser.openElements[1].attrs:
-              parser.openElements[1].attr(k, v)
+          parser.addAttrsIfMissing(parser.openElements[1], token.attrs)
       )
       "<frameset>" => (block:
         parse_error
-        if parser.openElements.len == 1 or parser.openElements[1].tagType != TAG_BODY or not parser.framesetOk:
+        if parser.openElements.len == 1 or
+            parser.getTagType(parser.openElements[1]) != TAG_BODY or
+            not parser.framesetOk:
           discard
         else:
           if parser.openElements[1].parentNode != nil:
@@ -1167,14 +1527,14 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
           discard # stop
       )
       "</body>" => (block:
-        if not parser.openElements.hasElementInScope(TAG_BODY):
+        if not parser.hasElementInScope(TAG_BODY):
           parse_error
         else:
           #NOTE parse error omitted
           parser.insertionMode = AFTER_BODY
       )
       "</html>" => (block:
-        if not parser.openElements.hasElementInScope(TAG_BODY):
+        if not parser.hasElementInScope(TAG_BODY):
           parse_error
         else:
           #NOTE parse error omitted
@@ -1185,31 +1545,31 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       "<details>", "<dialog>", "<dir>", "<div>", "<dl>", "<fieldset>",
       "<figcaption>", "<figure>", "<footer>", "<header>", "<hgroup>", "<main>",
       "<menu>", "<nav>", "<ol>", "<p>", "<section>", "<summary>", "<ul>") => (block:
-        if parser.openElements.hasElementInButtonScope(TAG_P):
+        if parser.hasElementInButtonScope(TAG_P):
           parser.closeP()
         discard parser.insertHTMLElement(token)
       )
       ("<h1>", "<h2>", "<h3>", "<h4>", "<h5>", "<h6>") => (block:
-        if parser.openElements.hasElementInButtonScope(TAG_P):
+        if parser.hasElementInButtonScope(TAG_P):
           parser.closeP()
-        if parser.currentNode.tagType in HTagTypes:
+        if parser.getTagType(parser.currentNode) in HTagTypes:
           parse_error
           pop_current_node
         discard parser.insertHTMLElement(token)
       )
       ("<pre>", "<listing>") => (block:
-        if parser.openElements.hasElementInButtonScope(TAG_P):
+        if parser.hasElementInButtonScope(TAG_P):
           parser.closeP()
         discard parser.insertHTMLElement(token)
         parser.ignoreLF = true
         parser.framesetOk = false
       )
       "<form>" => (block:
-        let hasTemplate = parser.openElements.hasElement(TAG_TEMPLATE)
+        let hasTemplate = parser.hasElement(TAG_TEMPLATE)
         if parser.form != nil and not hasTemplate:
           parse_error
         else:
-          if parser.openElements.hasElementInButtonScope(TAG_P):
+          if parser.hasElementInButtonScope(TAG_P):
             parser.closeP()
           let element = parser.insertHTMLElement(token)
           if not hasTemplate:
@@ -1219,16 +1579,18 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         parser.framesetOk = false
         for i in countdown(parser.openElements.high, 0):
           let node = parser.openElements[i]
-          case node.tagType
+          let tagType = parser.getTagType(node)
+          case tagType
           of TAG_LI:
             parser.generateImpliedEndTags(TAG_LI)
-            if parser.currentNode.tagType != TAG_LI: parse_error
-            while parser.popElement().tagType != TAG_LI: discard
+            if parser.getTagType(parser.currentNode) != TAG_LI:
+              parse_error
+            parser.popElementsIncl(TAG_LI)
             break
           of SpecialElements - {TAG_ADDRESS, TAG_DIV, TAG_P, TAG_LI}:
             break
           else: discard
-        if parser.openElements.hasElementInButtonScope(TAG_P):
+        if parser.hasElementInButtonScope(TAG_P):
           parser.closeP()
         discard parser.insertHTMLElement(token)
       )
@@ -1236,35 +1598,38 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         parser.framesetOk = false
         for i in countdown(parser.openElements.high, 0):
           let node = parser.openElements[i]
-          case node.tagType
+          let tagType = parser.getTagType(node)
+          case tagType
           of TAG_DD:
             parser.generateImpliedEndTags(TAG_DD)
-            if parser.currentNode.tagType != TAG_DD: parse_error
-            while parser.popElement().tagType != TAG_DD: discard
+            if parser.getTagType(parser.currentNode) != TAG_DD:
+              parse_error
+            parser.popElementsIncl(TAG_DD)
             break
           of TAG_DT:
             parser.generateImpliedEndTags(TAG_DT)
-            if parser.currentNode.tagType != TAG_DT: parse_error
-            while parser.popElement().tagType != TAG_DT: discard
+            if parser.getTagType(parser.currentNode) != TAG_DT:
+              parse_error
+            parser.popElementsIncl(TAG_DT)
             break
           of SpecialElements - {TAG_ADDRESS, TAG_DIV, TAG_P, TAG_DD, TAG_DT}:
             break
           else: discard
-        if parser.openElements.hasElementInButtonScope(TAG_P):
+        if parser.hasElementInButtonScope(TAG_P):
           parser.closeP()
         discard parser.insertHTMLElement(token)
       )
       "<plaintext>" => (block:
-        if parser.openElements.hasElementInButtonScope(TAG_P):
+        if parser.hasElementInButtonScope(TAG_P):
           parser.closeP()
         discard parser.insertHTMLElement(token)
         parser.tokenizer.state = PLAINTEXT
       )
       "<button>" => (block:
-        if parser.openElements.hasElementInScope(TAG_BUTTON):
+        if parser.hasElementInScope(TAG_BUTTON):
           parse_error
           parser.generateImpliedEndTags()
-          while parser.popElement().tagType != TAG_BUTTON: discard
+          parser.popElementsIncl(TAG_BUTTON)
         parser.reconstructActiveFormatting()
         discard parser.insertHTMLElement(token)
         parser.framesetOk = false
@@ -1274,72 +1639,79 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
        "</fieldset>", "</figcaption>", "</figure>", "</footer>", "</header>",
        "</hgroup>", "</listing>", "</main>", "</menu>", "</nav>", "</ol>",
        "</pre>", "</section>", "</summary>", "</ul>") => (block:
-        if not parser.openElements.hasElementInScope(token.tagtype):
+        if not parser.hasElementInScope(token.tagtype):
           parse_error
         else:
           parser.generateImpliedEndTags()
-          if parser.currentNode.tagType != token.tagtype: parse_error
-          while parser.popElement().tagType != token.tagtype: discard
+          if parser.getTagType(parser.currentNode) != token.tagtype:
+            parse_error
+          parser.popElementsIncl(token.tagtype)
       )
       "</form>" => (block:
-        if not parser.openElements.hasElement(TAG_TEMPLATE):
+        if not parser.hasElement(TAG_TEMPLATE):
           let node = parser.form
           parser.form = nil
-          if node == nil or not parser.openElements.hasElementInScope(node.tagType):
+          if node == nil or
+              not parser.hasElementInScope(parser.getTagType(node)):
             parse_error
             return
           parser.generateImpliedEndTags()
-          if parser.currentNode != node: parse_error
+          if parser.currentNode != node:
+            parse_error
           parser.openElements.delete(parser.openElements.find(node))
         else:
-          if not parser.openElements.hasElementInScope(TAG_FORM):
+          if not parser.hasElementInScope(TAG_FORM):
             parse_error
             return
           parser.generateImpliedEndTags()
-          if parser.currentNode.tagType != TAG_FORM: parse_error
-          while parser.popElement().tagType != TAG_FORM: discard
+          if parser.getTagType(parser.currentNode) != TAG_FORM:
+            parse_error
+          parser.popElementsIncl(TAG_FORM)
       )
       "</p>" => (block:
-        if not parser.openElements.hasElementInButtonScope(TAG_P):
+        if not parser.hasElementInButtonScope(TAG_P):
           parse_error
           discard parser.insertHTMLElement(Token(t: START_TAG, tagtype: TAG_P))
         parser.closeP()
       )
       "</li>" => (block:
-        if not parser.openElements.hasElementInListItemScope(TAG_LI):
+        if not parser.hasElementInListItemScope(TAG_LI):
           parse_error
         else:
           parser.generateImpliedEndTags(TAG_LI)
-          if parser.currentNode.tagType != TAG_LI: parse_error
-          while parser.popElement().tagType != TAG_LI: discard
+          if parser.getTagType(parser.currentNode) != TAG_LI:
+            parse_error
+          parser.popElementsIncl(TAG_LI)
       )
       ("</dd>", "</dt>") => (block:
-        if not parser.openElements.hasElementInScope(token.tagtype):
+        if not parser.hasElementInScope(token.tagtype):
           parse_error
         else:
           parser.generateImpliedEndTags(token.tagtype)
-          if parser.currentNode.tagType != token.tagtype: parse_error
-          while parser.popElement().tagType != token.tagtype: discard
+          if parser.getTagType(parser.currentNode) != token.tagtype:
+            parse_error
+          parser.popElementsIncl(token.tagtype)
       )
       ("</h1>", "</h2>", "</h3>", "</h4>", "</h5>", "</h6>") => (block:
-        if not parser.openElements.hasElementInScope(HTagTypes):
+        if not parser.hasElementInScope(HTagTypes):
           parse_error
         else:
           parser.generateImpliedEndTags()
-          if parser.currentNode.tagType != token.tagtype: parse_error
-          while parser.popElement().tagType notin HTagTypes: discard
+          if parser.getTagType(parser.currentNode) != token.tagtype:
+            parse_error
+          parser.popElementsIncl(HTagTypes)
       )
       "</sarcasm>" => (block:
         #*deep breath*
         anything_else
       )
       "<a>" => (block:
-        var anchor: Element = nil
+        var anchor: Handle = nil
         for i in countdown(parser.activeFormatting.high, 0):
           let format = parser.activeFormatting[i]
           if format[0] == nil:
             break
-          if format[0].tagType == TAG_A:
+          if parser.getTagType(format[0]) == TAG_A:
             anchor = format[0]
             break
         if anchor != nil:
@@ -1367,7 +1739,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       )
       "<nobr>" => (block:
         parser.reconstructActiveFormatting()
-        if parser.openElements.hasElementInScope(TAG_NOBR):
+        if parser.hasElementInScope(TAG_NOBR):
           parse_error
           if parser.adoptionAgencyAlgorithm(token):
             any_other_end_tag
@@ -1390,17 +1762,18 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         parser.framesetOk = false
       )
       ("</applet>", "</marquee>", "</object>") => (block:
-        if not parser.openElements.hasElementInScope(token.tagtype):
+        if not parser.hasElementInScope(token.tagtype):
           parse_error
         else:
           parser.generateImpliedEndTags()
-          if parser.currentNode.tagType != token.tagtype: parse_error
-          while parser.popElement().tagType != token.tagtype: discard
+          if parser.getTagType(parser.currentNode) != token.tagtype:
+            parse_error
+          while parser.getTagType(parser.popElement()) != token.tagtype: discard
           parser.clearActiveFormattingTillMarker()
       )
       "<table>" => (block:
-        if parser.document.mode != QUIRKS:
-          if parser.openElements.hasElementInButtonScope(TAG_P):
+        if parser.quirksMode != QUIRKS:
+          if parser.hasElementInButtonScope(TAG_P):
             parser.closeP()
         discard parser.insertHTMLElement(token)
         parser.framesetOk = false
@@ -1408,7 +1781,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       )
       "</br>" => (block:
         parse_error
-        parser.processInHTMLContent(Token(t: START_TAG, tagtype: TAG_BR))
+        reprocess Token(t: START_TAG, tagtype: TAG_BR)
       )
       ("<area>", "<br>", "<embed>", "<img>", "<keygen>", "<wbr>") => (block:
         parser.reconstructActiveFormatting()
@@ -1428,7 +1801,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         pop_current_node
       )
       "<hr>" => (block:
-        if parser.openElements.hasElementInButtonScope(TAG_P):
+        if parser.hasElementInButtonScope(TAG_P):
           parser.closeP()
         discard parser.insertHTMLElement(token)
         pop_current_node
@@ -1448,7 +1821,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         parser.insertionMode = TEXT
       )
       "<xmp>" => (block:
-        if parser.openElements.hasElementInButtonScope(TAG_P):
+        if parser.hasElementInButtonScope(TAG_P):
           parser.closeP()
         parser.reconstructActiveFormatting()
         parser.framesetOk = false
@@ -1462,7 +1835,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         parser.genericRawtextElementParsingAlgorithm(token)
       )
       "<noscript>" => (block:
-        if parser.scripting:
+        if parser.opts.scripting:
           parser.genericRawtextElementParsingAlgorithm(token)
         else:
           any_other_start_tag
@@ -1477,21 +1850,21 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
           parser.insertionMode = IN_SELECT
       )
       ("<optgroup>", "<option>") => (block:
-        if parser.currentNode.tagType == TAG_OPTION:
+        if parser.getTagType(parser.currentNode) == TAG_OPTION:
           pop_current_node
         parser.reconstructActiveFormatting()
         discard parser.insertHTMLElement(token)
       )
       ("<rb>", "<rtc>") => (block:
-        if parser.openElements.hasElementInScope(TAG_RUBY):
+        if parser.hasElementInScope(TAG_RUBY):
           parser.generateImpliedEndTags()
-          if parser.currentNode.tagType != TAG_RUBY: parse_error
+          if parser.getTagType(parser.currentNode) != TAG_RUBY: parse_error
         discard parser.insertHTMLElement(token)
       )
       ("<rp>", "<rt>") => (block:
-        if parser.openElements.hasElementInScope(TAG_RUBY):
+        if parser.hasElementInScope(TAG_RUBY):
           parser.generateImpliedEndTags(TAG_RTC)
-          if parser.currentNode.tagType notin {TAG_RUBY, TAG_RTC}: parse_error
+          if parser.getTagType(parser.currentNode) notin {TAG_RUBY, TAG_RTC}: parse_error
         discard parser.insertHTMLElement(token)
       )
       #NOTE <math> (not implemented)
@@ -1512,23 +1885,16 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       )
       TokenType.EOF => (block:
         parse_error
-        if parser.currentNode.tagType == TAG_SCRIPT:
-          HTMLScriptElement(parser.currentNode).alreadyStarted = true
+        if parser.getTagType(parser.currentNode) == TAG_SCRIPT:
+          parser.setScriptAlreadyStarted(parser.currentNode)
         pop_current_node
         parser.insertionMode = parser.oldInsertionMode
         reprocess token
       )
       "</script>" => (block:
-        #TODO microtask
-        let script = HTMLScriptElement(parser.popElement())
+        #TODO microtask (?)
+        pop_current_node
         parser.insertionMode = parser.oldInsertionMode
-        #TODO document.write() (?)
-        script.prepare()
-        while parser.document.parserBlockingScript != nil:
-          let script = parser.document.parserBlockingScript
-          parser.document.parserBlockingScript = nil
-          #TODO style sheet
-          script.execute()
       )
       TokenType.END_TAG => (block:
         pop_current_node
@@ -1537,12 +1903,12 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
 
   of IN_TABLE:
     template clear_the_stack_back_to_a_table_context() =
-      while parser.currentNode.tagType notin {TAG_TABLE, TAG_TEMPLATE, TAG_HTML}:
+      while parser.getTagType(parser.currentNode) notin {TAG_TABLE, TAG_TEMPLATE, TAG_HTML}:
         pop_current_node
 
     match token:
       (TokenType.CHARACTER_ASCII, TokenType.CHARACTER) => (block:
-        if parser.currentNode.tagType in {TAG_TABLE, TAG_TBODY, TAG_TFOOT, TAG_THEAD, TAG_TR}:
+        if parser.getTagType(parser.currentNode) in {TAG_TABLE, TAG_TBODY, TAG_TFOOT, TAG_THEAD, TAG_TR}:
           parser.pendingTableChars = ""
           parser.pendingTableCharsWhitespace = true
           parser.oldInsertionMode = parser.insertionMode
@@ -1556,7 +1922,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       )
       TokenType.COMMENT => (block: parser.insertComment(token))
       TokenType.DOCTYPE => (block: parse_error)
-      "<caption>" => (block: 
+      "<caption>" => (block:
         clear_the_stack_back_to_a_table_context
         parser.activeFormatting.add((nil, nil))
         discard parser.insertHTMLElement(token)
@@ -1580,18 +1946,18 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       )
       "<table>" => (block:
         parse_error
-        if not parser.openElements.hasElementInScope(TAG_TABLE):
+        if not parser.hasElementInScope(TAG_TABLE):
           discard
         else:
-          while parser.popElement().tagType != TAG_TABLE: discard
+          while parser.getTagType(parser.popElement()) != TAG_TABLE: discard
           parser.resetInsertionMode()
           reprocess token
       )
       "</table>" => (block:
-        if not parser.openElements.hasElementInScope(TAG_TABLE):
+        if not parser.hasElementInScope(TAG_TABLE):
           parse_error
         else:
-          while parser.popElement().tagType != TAG_TABLE: discard
+          while parser.getTagType(parser.popElement()) != TAG_TABLE: discard
           parser.resetInsertionMode()
       )
       ("</body>", "</caption>", "</col>", "</colgroup>", "</html>", "</tbody>",
@@ -1615,7 +1981,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       )
       "<form>" => (block:
         parse_error
-        if parser.form != nil or parser.openElements.hasElement(TAG_TEMPLATE):
+        if parser.form != nil or parser.hasElement(TAG_TEMPLATE):
           discard
         else:
           parser.form = HTMLFormElement(parser.insertHTMLElement(token))
@@ -1661,22 +2027,22 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
   of IN_CAPTION:
     match token:
       "</caption>" => (block:
-        if not parser.openElements.hasElementInTableScope(TAG_CAPTION):
+        if not parser.hasElementInTableScope(TAG_CAPTION):
           parse_error
         else:
           parser.generateImpliedEndTags()
-          if parser.currentNode.tagType != TAG_CAPTION: parse_error
-          while parser.popElement().tagType != TAG_CAPTION: discard
+          if parser.getTagType(parser.currentNode) != TAG_CAPTION: parse_error
+          while parser.getTagType(parser.popElement()) != TAG_CAPTION: discard
           parser.clearActiveFormattingTillMarker()
           parser.insertionMode = IN_TABLE
       )
       ("<caption>", "<col>", "<colgroup>", "<tbody>", "<td>", "<tfoot>",
        "<th>", "<thead>", "<tr>", "</table>") => (block:
-        if not parser.openElements.hasElementInTableScope(TAG_CAPTION):
+        if not parser.hasElementInTableScope(TAG_CAPTION):
           parse_error
         else:
           parser.generateImpliedEndTags()
-          if parser.currentNode.tagType != TAG_CAPTION: parse_error
+          if parser.getTagType(parser.currentNode) != TAG_CAPTION: parse_error
           parser.clearActiveFormattingTillMarker()
           parser.insertionMode = IN_TABLE
           reprocess token
@@ -1696,7 +2062,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         pop_current_node
       )
       "</colgroup>" => (block:
-        if parser.currentNode.tagType != TAG_COLGROUP:
+        if parser.getTagType(parser.currentNode) != TAG_COLGROUP:
           parse_error
         else:
           pop_current_node
@@ -1708,7 +2074,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       )
       TokenType.EOF => (block: parser.processInHTMLContent(token, IN_BODY))
       _ => (block:
-        if parser.currentNode.tagType != TAG_COLGROUP:
+        if parser.getTagType(parser.currentNode) != TAG_COLGROUP:
           parse_error
         else:
           pop_current_node
@@ -1718,7 +2084,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
 
   of IN_TABLE_BODY:
     template clear_the_stack_back_to_a_table_body_context() =
-      while parser.currentNode.tagType notin {TAG_TBODY, TAG_TFOOT, TAG_THEAD, TAG_TEMPLATE, TAG_HTML}:
+      while parser.getTagType(parser.currentNode) notin {TAG_TBODY, TAG_TFOOT, TAG_THEAD, TAG_TEMPLATE, TAG_HTML}:
         pop_current_node
 
     match token:
@@ -1735,7 +2101,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         reprocess token
       )
       ("</tbody>", "</tfoot>", "</thead>") => (block:
-        if not parser.openElements.hasElementInTableScope(token.tagtype):
+        if not parser.hasElementInTableScope(token.tagtype):
           parse_error
         else:
           clear_the_stack_back_to_a_table_body_context
@@ -1744,7 +2110,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       )
       ("<caption>", "<col>", "<colgroup>", "<tbody>", "<tfoot>", "<thead>",
        "</table>") => (block:
-        if not parser.openElements.hasElementInTableScope({TAG_TBODY, TAG_THEAD, TAG_TFOOT}):
+        if not parser.hasElementInTableScope({TAG_TBODY, TAG_THEAD, TAG_TFOOT}):
           parse_error
         else:
           clear_the_stack_back_to_a_table_body_context
@@ -1760,7 +2126,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
 
   of IN_ROW:
     template clear_the_stack_back_to_a_table_row_context() =
-      while parser.currentNode.tagType notin {TAG_TR, TAG_TEMPLATE, TAG_HTML}:
+      while parser.getTagType(parser.currentNode) notin {TAG_TR, TAG_TEMPLATE, TAG_HTML}:
         pop_current_node
 
     match token:
@@ -1771,7 +2137,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         parser.activeFormatting.add((nil, nil))
       )
       "</tr>" => (block:
-        if not parser.openElements.hasElementInTableScope(TAG_TR):
+        if not parser.hasElementInTableScope(TAG_TR):
           parse_error
         else:
           clear_the_stack_back_to_a_table_row_context
@@ -1780,7 +2146,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       )
       ("<caption>", "<col>", "<colgroup>", "<tbody>", "<tfoot>", "<thead>",
        "<tr>", "</table>") => (block:
-        if not parser.openElements.hasElementInTableScope(TAG_TR):
+        if not parser.hasElementInTableScope(TAG_TR):
           parse_error
         else:
           clear_the_stack_back_to_a_table_row_context
@@ -1789,9 +2155,9 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
           reprocess token
       )
       ("</tbody>", "</tfoot>", "</thead>") => (block:
-        if not parser.openElements.hasElementInTableScope(token.tagtype):
+        if not parser.hasElementInTableScope(token.tagtype):
           parse_error
-        elif not parser.openElements.hasElementInTableScope(TAG_TR):
+        elif not parser.hasElementInTableScope(TAG_TR):
           discard
         else:
           clear_the_stack_back_to_a_table_row_context
@@ -1806,25 +2172,26 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
   of IN_CELL:
     template close_cell() =
       parser.generateImpliedEndTags()
-      if parser.currentNode.tagType notin {TAG_TD, TAG_TH}: parse_error
-      while parser.popElement().tagType notin {TAG_TD, TAG_TH}: discard
+      if parser.getTagType(parser.currentNode) notin {TAG_TD, TAG_TH}: parse_error
+      while parser.getTagType(parser.popElement()) notin {TAG_TD, TAG_TH}: discard
       parser.clearActiveFormattingTillMarker()
       parser.insertionMode = IN_ROW
 
     match token:
       ("</td>", "</th>") => (block:
-        if not parser.openElements.hasElementInTableScope(token.tagtype):
+        if not parser.hasElementInTableScope(token.tagtype):
           parse_error
         else:
           parser.generateImpliedEndTags()
-          if parser.currentNode.tagType != token.tagtype: parse_error
-          while parser.popElement().tagType != token.tagtype: discard
+          if parser.getTagType(parser.currentNode) != token.tagtype:
+            parse_error
+          while parser.getTagType(parser.popElement()) != token.tagtype: discard
           parser.clearActiveFormattingTillMarker()
           parser.insertionMode = IN_ROW
       )
       ("<caption>", "<col>", "<colgroup>", "<tbody>", "<td>", "<tfoot>",
        "<thead>", "<tr>") => (block:
-        if not parser.openElements.hasElementInTableScope({TAG_TD, TAG_TH}):
+        if not parser.hasElementInTableScope({TAG_TD, TAG_TH}):
           parse_error
         else:
           close_cell
@@ -1833,7 +2200,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       ("</body>", "</caption>", "</col>", "</colgroup>",
        "</html>") => (block: parse_error)
       ("</table>", "</tbody>", "</tfoot>", "</thead>", "</tr>") => (block:
-        if not parser.openElements.hasElementInTableScope(token.tagtype):
+        if not parser.hasElementInTableScope(token.tagtype):
           parse_error
         else:
           close_cell
@@ -1849,51 +2216,51 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       TokenType.DOCTYPE => (block: parse_error)
       "<html>" => (block: parser.processInHTMLContent(token, IN_BODY))
       "<option>" => (block:
-        if parser.currentNode.tagType == TAG_OPTION:
+        if parser.getTagType(parser.currentNode) == TAG_OPTION:
           pop_current_node
         discard parser.insertHTMLElement(token)
       )
       "<optgroup>" => (block:
-        if parser.currentNode.tagType == TAG_OPTION:
+        if parser.getTagType(parser.currentNode) == TAG_OPTION:
           pop_current_node
-        if parser.currentNode.tagType == TAG_OPTGROUP:
+        if parser.getTagType(parser.currentNode) == TAG_OPTGROUP:
           pop_current_node
         discard parser.insertHTMLElement(token)
       )
       "</optgroup>" => (block:
-        if parser.currentNode.tagType == TAG_OPTION:
-          if parser.openElements.len > 1 and parser.openElements[^2].tagType == TAG_OPTGROUP:
+        if parser.getTagType(parser.currentNode) == TAG_OPTION:
+          if parser.openElements.len > 1 and parser.getTagType(parser.openElements[^2]) == TAG_OPTGROUP:
             pop_current_node
-        if parser.currentNode.tagType == TAG_OPTGROUP:
+        if parser.getTagType(parser.currentNode) == TAG_OPTGROUP:
           pop_current_node
         else:
           parse_error
       )
       "</option>" => (block:
-        if parser.currentNode.tagType == TAG_OPTION:
+        if parser.getTagType(parser.currentNode) == TAG_OPTION:
           pop_current_node
         else:
           parse_error
       )
       "</select>" => (block:
-        if not parser.openElements.hasElementInSelectScope(TAG_SELECT):
+        if not parser.hasElementInSelectScope(TAG_SELECT):
           parse_error
         else:
-          while parser.popElement().tagType != TAG_SELECT: discard
+          while parser.getTagType(parser.popElement()) != TAG_SELECT: discard
           parser.resetInsertionMode()
       )
       "<select>" => (block:
         parse_error
-        if parser.openElements.hasElementInSelectScope(TAG_SELECT):
-          while parser.popElement().tagType != TAG_SELECT: discard
+        if parser.hasElementInSelectScope(TAG_SELECT):
+          while parser.getTagType(parser.popElement()) != TAG_SELECT: discard
           parser.resetInsertionMode()
       )
       ("<input>", "<keygen>", "<textarea>") => (block:
         parse_error
-        if not parser.openElements.hasElementInSelectScope(TAG_SELECT):
+        if not parser.hasElementInSelectScope(TAG_SELECT):
           discard
         else:
-          while parser.popElement().tagType != TAG_SELECT: discard
+          while parser.getTagType(parser.popElement()) != TAG_SELECT: discard
           parser.resetInsertionMode()
           reprocess token
       )
@@ -1906,17 +2273,17 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       ("<caption>", "<table>", "<tbody>", "<tfoot>", "<thead>", "<tr>", "<td>",
        "<th>") => (block:
         parse_error
-        while parser.popElement().tagType != TAG_SELECT: discard
+        while parser.getTagType(parser.popElement()) != TAG_SELECT: discard
         parser.resetInsertionMode()
         reprocess token
       )
       ("</caption>", "</table>", "</tbody>", "</tfoot>", "</thead>", "</tr>",
        "</td>", "</th>") => (block:
         parse_error
-        if not parser.openElements.hasElementInTableScope(token.tagtype):
+        if not parser.hasElementInTableScope(token.tagtype):
           discard
         else:
-          while parser.popElement().tagType != TAG_SELECT: discard
+          while parser.getTagType(parser.popElement()) != TAG_SELECT: discard
           parser.resetInsertionMode()
           reprocess token
       )
@@ -1963,11 +2330,11 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       )
       TokenType.END_TAG => (block: parse_error)
       TokenType.EOF => (block:
-        if not parser.openElements.hasElement(TAG_TEMPLATE):
+        if not parser.hasElement(TAG_TEMPLATE):
           discard # stop
         else:
           parse_error
-          while parser.popElement().tagType != TAG_TEMPLATE: discard
+          parser.popElementsIncl(TAG_TEMPLATE)
           parser.clearActiveFormattingTillMarker()
           discard parser.templateModes.pop()
           parser.resetInsertionMode()
@@ -2000,11 +2367,12 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       TokenType.DOCTYPE => (block: parse_error)
       "<html>" => (block: parser.processInHTMLContent(token, IN_BODY))
       "<frameset>" => (block:
-        if parser.currentNode == parser.document.html:
+        if parser.getTagType(parser.currentNode) == TAG_HTML:
           parse_error
         else:
           pop_current_node
-        if not parser.fragment and parser.currentNode.tagType != TAG_FRAMESET:
+        if not parser.fragment and
+            parser.getTagType(parser.currentNode) != TAG_FRAMESET:
           parser.insertionMode = AFTER_FRAMESET
       )
       "<frame>" => (block:
@@ -2013,7 +2381,8 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       )
       "<noframes>" => (block: parser.processInHTMLContent(token, IN_HEAD))
       TokenType.EOF => (block:
-        if parser.currentNode != parser.document.html: parse_error
+        if parser.getTagType(parser.currentNode) != TAG_HTML:
+          parse_error
         # stop
       )
       _ => (block: parse_error)
@@ -2031,8 +2400,12 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
 
   of AFTER_AFTER_BODY:
     match token:
-      TokenType.COMMENT => (block: parser.insertComment(token, last_child_of(parser.document)))
-      (TokenType.DOCTYPE, AsciiWhitespace, "<html>") => (block: parser.processInHTMLContent(token, IN_BODY))
+      TokenType.COMMENT => (block:
+        parser.insertComment(token, last_child_of(parser.document))
+      )
+      (TokenType.DOCTYPE, AsciiWhitespace, "<html>") => (block:
+        parser.processInHTMLContent(token, IN_BODY)
+      )
       TokenType.EOF => (block: discard) # stop
       _ => (block:
         parse_error
@@ -2048,6 +2421,46 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       "<noframes>" => (block: parser.processInHTMLContent(token, IN_HEAD))
       _ => (block: parse_error)
 
+const CaseTable = {
+  "altglyph": "altGlyph",
+  "altglyphdef": "altGlyphDef",
+  "altglyphitem": "altGlyphItem",
+  "animatecolor": "animateColor",
+  "animatemotion": "animateMotion",
+  "animatetransform": "animateTransform",
+  "clippath": "clipPath",
+  "feblend": "feBlend",
+  "fecolormatrix": "feColorMatrix",
+  "fecomponenttransfer": "feComponentTransfer",
+  "fecomposite": "feComposite",
+  "feconvolvematrix": "feConvolveMatrix",
+  "fediffuselighting": "feDiffuseLighting",
+  "fedisplacementmap": "feDisplacementMap",
+  "fedistantlight": "feDistantLight",
+  "fedropshadow": "feDropShadow",
+  "feflood": "feFlood",
+  "fefunca": "feFuncA",
+  "fefuncb": "feFuncB",
+  "fefuncg": "feFuncG",
+  "fefuncr": "feFuncR",
+  "fegaussianblur": "feGaussianBlur",
+  "feimage": "feImage",
+  "femerge": "feMerge",
+  "femergenode": "feMergeNode",
+  "femorphology": "feMorphology",
+  "feoffset": "feOffset",
+  "fepointlight": "fePointLight",
+  "fespecularlighting": "feSpecularLighting",
+  "fespotlight": "feSpotLight",
+  "fetile": "feTile",
+  "feturbulence": "feTurbulence",
+  "foreignobject": "foreignObject",
+  "glyphref": "glyphRef",
+  "lineargradient": "linearGradient",
+  "radialgradient": "radialGradient",
+  "textpath": "textPath",
+}.toTable()
+
 proc processInForeignContent(parser: var HTML5Parser, token: Token) =
   macro `=>`(v: typed, body: untyped): untyped =
     quote do:
@@ -2058,53 +2471,17 @@ proc processInForeignContent(parser: var HTML5Parser, token: Token) =
     #TODO SVG
 
   template any_other_end_tag() =
-    if parser.currentNode.localName != token.tagname: parse_error
+    if parser.getLocalName(parser.currentNode) != token.tagname:
+      parse_error
     for i in countdown(parser.openElements.high, 1):
       let node = parser.openElements[i]
-      if node.localName == token.tagname:
-        while parser.popElement() != node: discard
+      if parser.getLocalName(parser.currentNode) == token.tagname:
+        while parser.popElement() != node:
+          discard
         break
-      if node.namespace == Namespace.HTML: break
-      parser.processInHTMLContent(token)
-  const CaseTable = {
-    "altglyph": "altGlyph",
-    "altglyphdef": "altGlyphDef",
-    "altglyphitem": "altGlyphItem",
-    "animatecolor": "animateColor",
-    "animatemotion": "animateMotion",
-    "animatetransform": "animateTransform",
-    "clippath": "clipPath",
-    "feblend": "feBlend",
-    "fecolormatrix": "feColorMatrix",
-    "fecomponenttransfer": "feComponentTransfer",
-    "fecomposite": "feComposite",
-    "feconvolvematrix": "feConvolveMatrix",
-    "fediffuselighting": "feDiffuseLighting",
-    "fedisplacementmap": "feDisplacementMap",
-    "fedistantlight": "feDistantLight",
-    "fedropshadow": "feDropShadow",
-    "feflood": "feFlood",
-    "fefunca": "feFuncA",
-    "fefuncb": "feFuncB",
-    "fefuncg": "feFuncG",
-    "fefuncr": "feFuncR",
-    "fegaussianblur": "feGaussianBlur",
-    "feimage": "feImage",
-    "femerge": "feMerge",
-    "femergenode": "feMergeNode",
-    "femorphology": "feMorphology",
-    "feoffset": "feOffset",
-    "fepointlight": "fePointLight",
-    "fespecularlighting": "feSpecularLighting",
-    "fespotlight": "feSpotLight",
-    "fetile": "feTile",
-    "feturbulence": "feTurbulence",
-    "foreignobject": "foreignObject",
-    "glyphref": "glyphRef",
-    "lineargradient": "linearGradient",
-    "radialgradient": "radialGradient",
-    "textpath": "textPath",
-  }.toTable()
+      if parser.getNamespace(node) == Namespace.HTML:
+        break
+      parser.processInHTMLContent(token, parser.insertionMode)
 
   match token:
     '\0' => (block:
@@ -2116,65 +2493,66 @@ proc processInForeignContent(parser: var HTML5Parser, token: Token) =
     TokenType.CHARACTER => (block: parser.insertCharacter(token.r))
     TokenType.DOCTYPE => (block: parse_error)
     ("<b>", "<big>", "<blockquote>", "<body>", "<br>", "<center>", "<code>",
-     "<dd>", "<div>", "<dl>", "<dt>", "<em>", "<embed>", "<h1>", "<h2>", "<h3>",
-     "<h4>", "<h5>", "<h6>", "<head>", "<hr>", "<i>", "<img>", "<li>",
+     "<dd>", "<div>", "<dl>", "<dt>", "<em>", "<embed>", "<h1>", "<h2>",
+     "<h3>", "<h4>", "<h5>", "<h6>", "<head>", "<hr>", "<i>", "<img>", "<li>",
      "<listing>", "<menu>", "<meta>", "<nobr>", "<ol>", "<p>", "<pre>",
      "<ruby>", "<s>", "<small>", "<span>", "<strong>", "<strike>", "<sub>",
      "<sup>", "<table>", "<tt>", "<u>", "<ul>", "<var>") => (block:
       parse_error
       #NOTE MathML not implemented
-      while not (parser.currentNode.isHTMLIntegrationPoint() or parser.currentNode.inHTMLNamespace()):
+      while not parser.isHTMLIntegrationPoint(parser.currentNode) and
+          parser.getNamespace(parser.currentNode) != Namespace.HTML:
         pop_current_node
-      parser.processInHTMLContent(token)
+      parser.processInHTMLContent(token, parser.insertionMode)
     )
     TokenType.START_TAG => (block:
       #NOTE MathML not implemented
-
-      if parser.adjustedCurrentNode.namespace == Namespace.SVG:
+      let namespace = parser.getNamespace(parser.adjustedCurrentNode)
+      if namespace == Namespace.SVG:
         if token.tagname in CaseTable:
           token.tagname = CaseTable[token.tagname]
         adjustSVGAttributes(token)
       #TODO adjust foreign attributes
-      let element = parser.insertForeignElement(token, parser.adjustedCurrentNode.namespace)
-      if token.selfclosing and element.inSVGNamespace():
+      discard parser.insertForeignElement(token, namespace)
+      if token.selfclosing and namespace == Namespace.SVG:
         script_end_tag
       else:
         pop_current_node
     )
     "</script>" => (block:
-      if parser.currentNode.namespace == Namespace.SVG and parser.currentNode.localName == "script": #TODO SVG
+      let namespace = parser.getNamespace(parser.currentNode)
+      let localName = parser.getLocalName(parser.currentNode)
+      if namespace == Namespace.SVG and localName == "script": #TODO SVG
         script_end_tag
       else:
         any_other_end_tag
     )
     TokenType.END_TAG => (block: any_other_end_tag)
 
-proc constructTree(parser: var HTML5Parser): Document =
+proc constructTree[Handle](parser: var HTML5Parser[Handle]) =
   for token in parser.tokenizer.tokenize:
     if parser.ignoreLF:
       parser.ignoreLF = false
       if token.t == CHARACTER_ASCII and token.c == '\n':
         continue
+    let isTokenHTML = token.t in {START_TAG, CHARACTER, CHARACTER_ASCII}
     if parser.openElements.len == 0 or
-       parser.adjustedCurrentNode.inHTMLNamespace() or
-       parser.adjustedCurrentNode.isHTMLIntegrationPoint() and token.t in {START_TAG, CHARACTER, CHARACTER_ASCII} or
+       parser.getNamespace(parser.adjustedCurrentNode) == Namespace.HTML or
+       parser.isHTMLIntegrationPoint(parser.adjustedCurrentNode) and
+        isTokenHTML or
        token.t == EOF:
       #NOTE MathML not implemented
-      parser.processInHTMLContent(token)
+      parser.processInHTMLContent(token, parser.insertionMode)
     else:
       parser.processInForeignContent(token)
     if parser.needsreinterpret:
       break
-  return parser.document
 
 proc finishParsing(parser: var HTML5Parser) =
   while parser.openElements.len > 0:
     pop_current_node
-  while parser.document.scriptsToExecOnLoad.len > 0:
-    #TODO spin event loop
-    let script = parser.document.scriptsToExecOnLoad.popFirst()
-    script.execute()
-  #TODO events
+  if parser.dombuilder.finish != nil:
+    parser.dombuilder.finish(parser.dombuilder)
 
 proc bomSniff(inputStream: Stream): Charset =
   # bom sniff
@@ -2193,25 +2571,31 @@ proc bomSniff(inputStream: Stream): Charset =
     else:
       inputStream.setPosition(0)
 
-proc parseHTML*(inputStream: Stream, charsets: seq[Charset] = @[],
-    fallbackcs = DefaultCharset, window: Window = nil, url: URL = nil,
-    canReinterpret = true): Document =
+proc parseHTML*[Handle](inputStream: Stream, dombuilder: DOMBuilder[Handle],
+    opts: HTML5ParserOpts[Handle]) =
+  ## Parse an HTML document, using the DOMBuilder object `dombuilder`, and
+  ## parser options `opts`.
   var charsetStack: seq[Charset]
-  for i in countdown(charsets.high, 0):
-    charsetStack.add(charsets[i])
-  var canReinterpret = canReinterpret
+  for i in countdown(opts.charsets.high, 0):
+    charsetStack.add(opts.charsets[i])
+  var canReinterpret = opts.canReinterpret
   var confidence: CharsetConfidence
-  let scs = inputStream.bomSniff()
-  if scs != CHARSET_UNKNOWN:
-    charsetStack.add(scs)
-    confidence = CONFIDENCE_CERTAIN
-  elif charsetStack.len == 0:
-    charsetStack.add(fallbackcs)
+  if canReinterpret:
+    let scs = inputStream.bomSniff()
+    if scs != CHARSET_UNKNOWN:
+      charsetStack.add(scs)
+      confidence = CONFIDENCE_CERTAIN
+  if charsetStack.len == 0:
+    charsetStack.add(DefaultCharset) # UTF-8
   while true:
-    var parser: HTML5Parser
-    parser.confidence = confidence
-    confidence = CONFIDENCE_TENTATIVE
-    parser.charset = charsetStack.pop()
+    let charset = charsetStack.pop()
+    var parser = HTML5Parser[Handle](
+      dombuilder: dombuilder,
+      confidence: confidence,
+      charset: charset,
+      opts: opts
+    )
+    confidence = CONFIDENCE_TENTATIVE # used in the next iteration
     if not canReinterpret:
       parser.confidence = CONFIDENCE_CERTAIN
     let em = if charsetStack.len == 0 or not canReinterpret:
@@ -2219,14 +2603,8 @@ proc parseHTML*(inputStream: Stream, charsets: seq[Charset] = @[],
     else:
       DECODER_ERROR_MODE_FATAL
     let decoder = newDecoderStream(inputStream, parser.charset, errormode = em)
-    parser.document = newDocument()
-    parser.document.contentType = "text/html"
-    if window != nil:
-      parser.document.window = window
-      window.document = parser.document
-    parser.document.url = url
     parser.tokenizer = newTokenizer(decoder)
-    let document = parser.constructTree()
+    parser.constructTree()
     if parser.needsreinterpret and canReinterpret:
       inputStream.setPosition(0)
       charsetStack.add(parser.charset)
@@ -2236,21 +2614,4 @@ proc parseHTML*(inputStream: Stream, charsets: seq[Charset] = @[],
       inputStream.setPosition(0)
       continue
     parser.finishParsing()
-    return document
-
-proc newDOMParser*(): DOMParser {.jsctor.} =
-  new(result)
-
-proc parseFromString(parser: DOMParser, str: string, t: string):
-    Result[Document, JSError] {.jsfunc.} =
-  case t
-  of "text/html":
-    let res = parseHTML(newStringStream(str))
-    return ok(res)
-  of "text/xml", "application/xml", "application/xhtml+xml", "image/svg+xml":
-    return err(newInternalError("XML parsing is not supported yet"))
-  else:
-    return err(newTypeError("Invalid mime type"))
-
-proc addHTMLModule*(ctx: JSContext) =
-  ctx.registerType(DOMParser)
+    break
diff --git a/src/html/tags.nim b/src/html/tags.nim
index d12972a0..ce78e178 100644
--- a/src/html/tags.nim
+++ b/src/html/tags.nim
@@ -49,6 +49,18 @@ type
     TAG_DIR, TAG_FONT, TAG_FRAME, TAG_NOFRAMES, TAG_FRAMESET, TAG_STRIKE,
     TAG_TT, TAG_TEMPLATE, TAG_SARCASM
 
+  QuirksMode* = enum
+    NO_QUIRKS, QUIRKS, LIMITED_QUIRKS
+
+  Namespace* = enum
+    NO_NAMESPACE = "",
+    HTML = "http://www.w3.org/1999/xhtml",
+    MATHML = "http://www.w3.org/1998/Math/MathML",
+    SVG = "http://www.w3.org/2000/svg",
+    XLINK = "http://www.w3.org/1999/xlink",
+    XML = "http://www.w3.org/XML/1998/namespace",
+    XMLNS = "http://www.w3.org/2000/xmlns/"
+
 func getTagTypeMap(): Table[string, TagType] =
   for i in TagType:
     let enumname = $TagType(i)
diff --git a/src/render/renderdocument.nim b/src/render/renderdocument.nim
index 6f8b2a80..b693d501 100644
--- a/src/render/renderdocument.nim
+++ b/src/render/renderdocument.nim
@@ -7,6 +7,7 @@ import css/sheet
 import css/stylednode
 import css/values
 import html/dom
+import html/tags
 import io/window
 import layout/box
 import layout/engine