about summary refs log blame commit diff stats
path: root/src/html/dom.nim
blob: 355fc3c1334eb23d4f9de4565e769c5b04f4e26f (plain) (tree)
1
2
3
4
5
6
7
8
9
             
             
              
           
              
               
             
 
                

                             
                

                 
                    

                    
                
                   
 
    







                                                                        



                                     
                      






                                                  


































                                                                             
    
                      

                                   




                                  







                             
 




                                                                             
                
 
                           
                    
                       



                               
                                      
 


                                   




                                                  
           




                                           




                           
                                   
                                 


                                    

                                     
              
                       





                                                                              
 
                            
                                   



                                    
 


                                
                                



                                                    
                     

                                    

                                               




                                                   


                                             
                   
                                  

                                        
 


                                                     


                                    
                                     
                           
 
                                     
 
                                        
 


                                                     
                            
 









                                        
                                    

                      

                     
                         
                                      

                                       
                
                  
 
                                      
 
                                                    


                                                         
                                    
                         
                            



                      
 
                                                
 
                                                          
                                    
 
                                              
 

                                                  
                                                
                   
  
                                                 

                 


                                              

                                               


                                               

                                            
                                 
 
                                               
                         
 
                                              
                         
 



                                              

                                
                                         
 









                                                  
                             
                         




                               
 



                                              
                                                          
                                    
                      



                                                            
                            
 

                                               
































































                                                                                        



                                                 


                                                       























                                                            
                               
                                                                                       


                               
                                           
                              
                                                           
                   
                                  


                                                   

                         
                                   

                                                 



                                                              
       
                                        
 

                                                       


                                      


                                                           



















                                                     
                 
                      
                          


                                               









                                                                  
 




                                                                       




                                                                     














                                                                            
       
                                           
                 
 
                                                  
                             

                                  

                                                                            
                                  

                                    
                                       
                                   
                                     
                                      
 








                                                            


                                               

                                                




                                                   

                                   













                                                                   
















                                                                                                                

                           
   
                         
                             









                                                             
































































































                                                                                                 





                                                              
                                                        
                      
                               
 



                                                                     
                                                         




                                                                      
                                                                   
                        



                                                                                
 
                                                                                               


                              
                               




                         



                                          

                                           










                                                                                            
 






                                                                                            

                                                                                      
                                                                  







                                                                                            

                                                                                                


                                                             
 
                                                                                      



                                             

                                                                                               


                                              



                                                
                                                               
                          






                                                                          






                                                                                 


                                                           






                                                   
































                                                                                




                                                                                          

                                             

                                             



                                           

                                             



                                           
 





                                                            
                                                         
                              



                                                     
                              
















                                                               























                                                       



                                  
                                                

                                                                  
                                    
                          
 





                                  
                                               
                             
              
                          
 
                                              
                             
              
                           
 
                                                         
                                

                
 
                                                        
                                

                
 
















                                                                  
                                                                         
                    
                                                                                             

                          






                                                                                                                            
 
                                                                              



                                                 
                                




                                            

                                      








                                                   





                                                                                                

                                                                 
                                              

               
                                                             

                 

            

                                                             
                                              
       

                                                             

                 

            


                                                              
                                                            
                                          







                                                       








                                                         








                                                          
                                                
                        


               
                                        
                                                      
                                 

                    
                                                      

                 












                                                                         
















                                                                         
 
                                                    
                      



                                 
                                                       
                    
                                                                       






                                     



                                                                                     
                   


                                                             



                                             
                       
                                                             
           
                                                    

                      

























                                                                                      

                                 

           
                                                   

                                    





                                                                           



                                           




                                                                             
 


                                                

                                   





                                                      

                                                        

                                      






                                                         
 

                                         

                                 
                                                  



                                






                                                                        
        







                                                     


















                                                               
         
                                                                          
                           















                                                                                             








                                                                   
 
                                                                      




                                 

                                                                                                





                                          
 




                                                                
 











                                                                         
 
                     
                                                                                                                                                               






                                   

                                     





                                                    

                                 



                                  

                                 

                               

                                  

                                 

                                 







                                                                                            

                                 

                                   

                                     

                                  
       
                             

                                

                                 
                            
                                                   
                                                         







                                                                                                                                                                                          

                                
 
                                          


                           
                          










                                                                                                     
 



















                                                                                                

                                        




                                          





                                              

                



                                                            
                                                                                     

                            


                                                                      




                                                    
                                                 

                                                                                             








                                                             
                                                           



                                                   


                                             







                                             
 


                                              
                      













                                                                               

                                          
                                               
                                     

















                                                                    
 
















                                                                 


                                                        

                                                          




                                                           
 







                                                                                            

                                    












                                                                                                  
                                     

                                                             
                                                

                                                                                         
 
                                                                          



                                                                                            


                                                                                   


                                                             
 

















                                                                                                           




                                              
                                                                                  











                                                                   
                                                                             

                        
                                                                                        







                                     







                                                                                                 




                                                  
                                                                       
                                                               

                              




                                               
                                



                                                       
                                         

                          
                 


                                                                               

                                                                                    
                                                            
 



                                                     
                                            
                                 

                            








                                                      

                                                        
                              



                                         
                                                       
                                             

                                                   
                                             
                               


                                                                                 
 











                                                
                        


                                           
                                             

















                                                                               



                                                











                                                                      



                                           




                                                   









                                                                           
                                                                                         
                                   


                                              
















                                                                                                                           
                                                  




                                                  






































                                                                                                                                                                    

                                          
                                                                        




                                             

                                               



                              

                                     


                               

                                                          
         

                                               
                                          


                                     
 
                                                                       
                                               



                                           
                                       





                                                        

                                  




















                                                       


                                                                         



                                                                
                                            







                                           
 
                                    

                               
                          
 


                                                                 
                                   



                                                                     












































                                                                                                             



                                                             
                            

                    


                            
 















                                                               

                                                                                               



                                                
                                                                                         













                                                                                  
                             














































































































                                                                                                
 



















                                                                                       




















                                                                                                                                       









                                                                                           
                                                                           







                                                                                                              
                                                          








                                                                        















































































                                                                                              



                                               






















































                                                                                                         


                                                               

                                  
                                              
                                     
                                

                                                                          


                                                                    

                                                   
                                          
                                
                               
import deques
import macros
import options
import sets
import streams
import strutils
import tables

import css/sheet
import data/charset
import encoding/decoderstream
import html/tags
import io/loader
import io/request
import js/javascript
import types/mime
import types/referer
import types/url
import utils/twtstr

type
  FormMethod* = enum
    FORM_METHOD_GET, FORM_METHOD_POST, FORM_METHOD_DIALOG

  FormEncodingType* = enum
    FORM_ENCODING_TYPE_URLENCODED = "application/x-www-form-urlencoded",
    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

  ParserMetadata = enum
    PARSER_INSERTED, NOT_PARSER_INSERTED

  ScriptResultType = enum
    RESULT_NULL, RESULT_UNINITIALIZED, RESULT_SCRIPT, RESULT_IMPORT_MAP_PARSE

type
  Script = object
    #TODO setings
    baseURL: URL
    options: ScriptOptions
    mutedErrors: bool
    #TODO parse error/error to rethrow
    record: string #TODO should be a record...

  ScriptOptions = object
    nonce: string
    integrity: string
    parserMetadata: ParserMetadata
    credentialsMode: CredentialsMode
    referrerPolicy: Option[ReferrerPolicy]
    renderBlocking: bool

  ScriptResult = object
    case t: ScriptResultType
    of RESULT_NULL, RESULT_UNINITIALIZED:
      discard
    of RESULT_SCRIPT:
      script: Script
    of RESULT_IMPORT_MAP_PARSE:
      discard #TODO

type
  Window* = ref object
    console* {.jsget.}: console
    navigator* {.jsget.}: Navigator
    settings*: EnvironmentSettings
    loader*: Option[FileLoader]
    jsrt*: JSRuntime
    jsctx*: JSContext
    document* {.jsget.}: Document

  # Navigator stuff
  Navigator* = ref object
    plugins: PluginArray

  PluginArray* = ref object

  MimeTypeArray* = ref object

  # "For historical reasons, console is lowercased."
  # Also, for a more practical reason: so the javascript macros don't confuse
  # this and the Client console.
  # TODO: merge those two
  console* = ref object
    err*: Stream

  NamedNodeMap = ref object
    element: Element
    attrlist: seq[Attr]

  EnvironmentSettings* = object
    scripting*: bool

  EventTarget* = ref object of RootObj

  Collection = ref CollectionObj
  CollectionObj = object of RootObj
    islive: bool
    childonly: bool
    root: Node
    match: proc(node: Node): bool {.noSideEffect.}
    snapshot: seq[Node]
    livelen: int
    id: int

  NodeList = ref object of Collection

  HTMLCollection = ref object of Collection

  DOMTokenList = ref object
    toks*: seq[string]
    element: Element
    localName: string

  Node* = ref object of EventTarget
    nodeType* {.jsget.}: NodeType
    childList*: seq[Node]
    nextSibling* {.jsget.}: Node
    previousSibling* {.jsget.}: Node
    parentNode* {.jsget.}: Node
    parentElement* {.jsget.}: Element
    root: Node
    document*: Document
    # Live collection cache: ids of live collections are saved in all
    # nodes they refer to. These are removed when the collection is destroyed,
    # and invalidated when the owner node's children or attributes change.
    # (We can't just store pointers, because those may be invalidated by
    # the JavaScript finalizers.)
    liveCollections: HashSet[int]

  Attr* = ref object of Node
    namespaceURI* {.jsget.}: string
    prefix* {.jsget.}: string
    localName* {.jsget.}: string
    value* {.jsget.}: string
    ownerElement* {.jsget.}: Element

  DOMImplementation = ref object
    document: Document

  Document* = ref object of Node
    charset*: Charset
    window*: Window
    url*: URL #TODO expose as URL (capitalized)
    location {.jsget.}: URL #TODO should be location
    mode*: QuirksMode
    currentScript: HTMLScriptElement
    isxml*: bool
    implementation {.jsget.}: DOMImplementation
    origin: Origin

    scriptsToExecSoon*: seq[HTMLScriptElement]
    scriptsToExecInOrder*: Deque[HTMLScriptElement]
    scriptsToExecOnLoad*: Deque[HTMLScriptElement]
    parserBlockingScript*: HTMLScriptElement

    parser_cannot_change_the_mode_flag*: bool
    is_iframe_srcdoc*: bool
    focus*: Element
    contentType* {.jsget.}: string

    renderBlockingElements: seq[Element]

    invalidCollections: HashSet[int] # collection ids
    colln: int

    cachedSheets: seq[CSSStylesheet]
    cachedSheetsInvalid: bool

  CharacterData* = ref object of Node
    data* {.jsget.}: string

  Text* = ref object of CharacterData

  Comment* = ref object of CharacterData

  CDATASection = ref object of CharacterData

  ProcessingInstruction = ref object of CharacterData
    target {.jsget.}: string

  DocumentFragment* = ref object of Node
    host*: Element

  DocumentType* = ref object of Node
    name*: string
    publicId*: string
    systemId*: string

  Element* = ref object of Node
    namespace*: Namespace
    namespacePrefix*: Option[string]
    prefix*: string
    localName*: string
    tagType*: TagType

    id* {.jsget.}: string
    classList* {.jsget.}: DOMTokenList
    attrs*: Table[string, string]
    attributes* {.jsget.}: NamedNodeMap
    hover*: bool
    invalid*: bool

  HTMLElement* = ref object of Element

  FormAssociatedElement* = ref object of HTMLElement
    parserInserted*: bool

  HTMLInputElement* = ref object of FormAssociatedElement
    form* {.jsget.}: HTMLFormElement
    inputType*: InputType
    value* {.jsget.}: string
    checked*: bool
    xcoord*: int
    ycoord*: int
    file*: Option[Url]

  HTMLAnchorElement* = ref object of HTMLElement

  HTMLSelectElement* = ref object of FormAssociatedElement
    form* {.jsget.}: HTMLFormElement

  HTMLSpanElement* = ref object of HTMLElement

  HTMLOptGroupElement* = ref object of HTMLElement

  HTMLOptionElement* = ref object of HTMLElement
    selected*: bool
  
  HTMLHeadingElement* = ref object of HTMLElement
    rank*: uint16

  HTMLBRElement* = ref object of HTMLElement

  HTMLMenuElement* = ref object of HTMLElement

  HTMLUListElement* = ref object of HTMLElement

  HTMLOListElement* = ref object of HTMLElement
    start*: Option[int]

  HTMLLIElement* = ref object of HTMLElement
    value* {.jsget.}: Option[int]

  HTMLStyleElement* = ref object of HTMLElement
    sheet*: CSSStylesheet

  HTMLLinkElement* = ref object of HTMLElement
    sheet*: CSSStylesheet

  HTMLFormElement* = ref object of HTMLElement
    name*: string
    smethod*: string
    enctype*: string
    novalidate*: bool
    constructingentrylist*: bool
    controls*: seq[FormAssociatedElement]

  HTMLTemplateElement* = ref object of HTMLElement
    content*: DocumentFragment

  HTMLUnknownElement* = ref object of HTMLElement

  HTMLScriptElement* = ref object of HTMLElement
    parserDocument*: Document
    preparationTimeDocument*: Document
    forceAsync*: bool
    fromAnExternalFile*: bool
    readyForParserExec*: bool
    alreadyStarted*: bool
    delayingTheLoadEvent: bool
    ctype: ScriptType
    internalNonce: string
    scriptResult*: ScriptResult
    onReady: (proc())

  HTMLBaseElement* = ref object of HTMLElement

  HTMLAreaElement* = ref object of HTMLElement

  HTMLButtonElement* = ref object of FormAssociatedElement
    form* {.jsget.}: HTMLFormElement
    ctype*: ButtonType
    value* {.jsget, jsset.}: string

  HTMLTextAreaElement* = ref object of FormAssociatedElement
    form* {.jsget.}: HTMLFormElement
    value* {.jsget.}: string

  HTMLLabelElement* = ref object of HTMLElement

# Reflected attributes.
type
  ReflectType = enum
    REFLECT_STR, REFLECT_BOOL, REFLECT_INT, REFLECT_INT_GREATER_ZERO,
    REFLECT_INT_GREATER_EQUAL_ZERO

  ReflectEntry = tuple[
    attrname: string,
    funcname: string,
    t: ReflectType,
    tags: set[TagType],
    i: int
  ]

template toset(ts: openarray[TagType]): set[TagType] =
  var tags: set[TagType]
  for tag in ts:
    tags.incl(tag)
  tags

template makes(name: string, ts: set[TagType]): ReflectEntry =
  (name, name, REFLECT_STR, ts, 0)

template makes(attrname: string, funcname: string, ts: set[TagType]): ReflectEntry =
  (attrname, funcname, REFLECT_STR, ts, 0)

template makes(name: string, ts: varargs[TagType]): ReflectEntry =
  makes(name, toset(ts))

template makes(attrname: string, funcname: string, ts: varargs[TagType]): ReflectEntry =
  makes(attrname, funcname, toset(ts))

template makeb(name: string, ts: varargs[TagType]): ReflectEntry =
  (name, name, REFLECT_BOOL, toset(ts), 0)

template makei(name: string, ts: varargs[TagType], default = 0): ReflectEntry =
  (name, name, REFLECT_INT, toset(ts), default)

template makeigz(name: string, ts: varargs[TagType], default = 0): ReflectEntry =
  (name, name, REFLECT_INT_GREATER_ZERO, toset(ts), default)

template makeigez(name: string, ts: varargs[TagType], default = 0): ReflectEntry =
  (name, name, REFLECT_INT_GREATER_EQUAL_ZERO, toset(ts), default)

const ReflectTable0 = [
  # non-global attributes
  makes("target", TAG_A, TAG_AREA, TAG_LABEL, TAG_LINK),
  makes("href", TAG_LINK),
  makeb("required", TAG_INPUT, TAG_SELECT, TAG_TEXTAREA),
  makes("rel", "relList", TAG_A, TAG_LINK, TAG_LABEL),
  makes("for", "htmlFor", TAG_LABEL),
  makeigz("cols", TAG_TEXTAREA, 20),
  makeigz("rows", TAG_TEXTAREA, 1),
# <SELECT>:
#> For historical reasons, the default value of the size IDL attribute does
#> not return the actual size used, which, in the absence of the size content
#> attribute, is either 1 or 4 depending on the presence of the multiple
#> attribute.
  makeigz("size", TAG_SELECT, 0),
  makeigz("size", TAG_INPUT, 20),
  # "super-global" attributes
  makes("slot", AllTagTypes),
  makes("class", "className", AllTagTypes)
]

# Forward declarations
func attrb*(element: Element, s: string): bool
proc attr*(element: Element, name, value: string)

proc tostr(ftype: enum): string =
  return ($ftype).split('_')[1..^1].join("-").tolower()

func escapeText(s: string, attribute_mode = false): string =
  var nbsp_mode = false
  var nbsp_prev: char
  for c in s:
    if nbsp_mode:
      if c == char(0xA0):
        result &= "&nbsp;"
      else:
        result &= nbsp_prev & c
      nbsp_mode = false
    elif c == '&':
      result &= "&amp;"
    elif c == char(0xC2):
      nbsp_mode = true
      nbsp_prev = c
    elif attribute_mode and c == '"':
      result &= "&quot;"
    elif not attribute_mode and c == '<':
      result &= "&lt;"
    elif not attribute_mode and c == '>':
      result &= "&gt;"
    else:
      result &= c

func `$`*(node: Node): string =
  if node == nil: return "null" #TODO this isn't standard compliant but helps debugging
  case node.nodeType
  of ELEMENT_NODE:
    let element = Element(node)
    result = "<" & $element.tagType.tostr()
    for k, v in element.attrs:
      result &= ' ' & k & "=\"" & v.escapeText(true) & "\""
    result &= ">\n"
    for node in element.childList:
      for line in ($node).split('\n'):
        result &= "\t" & line & "\n"
    result &= "</" & $element.tagType.tostr() & ">"
  of TEXT_NODE:
    let text = Text(node)
    result = text.data.escapeText()
  of COMMENT_NODE:
    result = "<!-- " & Comment(node).data & "-->"
  of PROCESSING_INSTRUCTION_NODE:
    result = "" #TODO
  of DOCUMENT_TYPE_NODE:
    result = "<!DOCTYPE" & ' ' & DocumentType(node).name & ">"
  else:
    result = "Node of " & $node.nodeType

iterator elementList*(node: Node): Element {.inline.} =
  for child in node.childList:
    if child.nodeType == ELEMENT_NODE:
      yield Element(child)

iterator elementList_rev*(node: Node): Element {.inline.} =
  for i in countdown(node.childList.high, 0):
    let child = node.childList[i]
    if child.nodeType == ELEMENT_NODE:
      yield Element(child)

# Returns the node's ancestors
iterator ancestors*(node: Node): Element {.inline.} =
  var element = node.parentElement
  while element != nil:
    yield element
    element = element.parentElement

# Returns the node itself and its ancestors
iterator branch*(node: Node): Node {.inline.} =
  var node = node
  while node != nil:
    yield node
    node = node.parentNode

# Returns the node's descendants
iterator descendants*(node: Node): Node {.inline.} =
  var stack: seq[Node]
  stack.add(node)
  while stack.len > 0:
    let node = stack.pop()
    for i in countdown(node.childList.high, 0):
      yield node.childList[i]
      stack.add(node.childList[i])

iterator elements*(node: Node): Element {.inline.} =
  for child in node.descendants:
    if child.nodeType == ELEMENT_NODE:
      yield Element(child)

iterator elements*(node: Node, tag: TagType): Element {.inline.} =
  for desc in node.elements:
    if desc.tagType == tag:
      yield desc

iterator elements*(node: Node, tag: set[TagType]): Element {.inline.} =
  for desc in node.elements:
    if desc.tagType in tag:
      yield desc

iterator inputs(form: HTMLFormElement): HTMLInputElement {.inline.} =
  for control in form.controls:
    if control.tagType == TAG_INPUT:
      yield HTMLInputElement(control)

iterator radiogroup(form: HTMLFormElement): HTMLInputElement {.inline.} =
  for input in form.inputs:
    if input.inputType == INPUT_RADIO:
      yield input

iterator radiogroup(document: Document): HTMLInputElement {.inline.} =
  for input in document.elements(TAG_INPUT):
    let input = HTMLInputElement(input)
    if input.form == nil and input.inputType == INPUT_RADIO:
      yield input

iterator radiogroup*(input: HTMLInputElement): HTMLInputElement {.inline.} =
  if input.form != nil:
    for input in input.form.radiogroup:
      yield input
  else:
    for input in input.document.radiogroup:
      yield input

iterator textNodes*(node: Node): Text {.inline.} =
  for node in node.childList:
    if node.nodeType == TEXT_NODE:
      yield Text(node)
  
iterator options*(select: HTMLSelectElement): HTMLOptionElement {.inline.} =
  for child in select.elementList:
    if child.tagType == TAG_OPTION:
      yield HTMLOptionElement(child)
    elif child.tagType == TAG_OPTGROUP:
      for opt in child.elementList:
        if opt.tagType == TAG_OPTION:
          yield HTMLOptionElement(opt)

proc populateCollection(collection: Collection) =
  if collection.childonly:
    for child in collection.root.childList:
      if collection.match == nil or collection.match(child):
        collection.snapshot.add(child)
  else:
    for desc in collection.root.descendants:
      if collection.match == nil or collection.match(desc):
        collection.snapshot.add(desc)
  if collection.islive:
    for child in collection.snapshot:
      child.liveCollections.incl(collection.id)

proc refreshCollection(collection: Collection) =
  let document = collection.root.document
  if collection.id in document.invalidCollections:
    for child in collection.snapshot:
      assert collection.id in child.liveCollections
      child.liveCollections.excl(collection.id)
    collection.snapshot.setLen(0)
    collection.populateCollection()
    document.invalidCollections.excl(collection.id)

proc finalize0(collection: Collection) =
  if collection.islive:
    for child in collection.snapshot:
      assert collection.id in child.liveCollections
      child.liveCollections.excl(collection.id)
    collection.root.document.invalidCollections.excl(collection.id)

proc finalize(collection: HTMLCollection) {.jsfin.} =
  collection.finalize0()

proc finalize(collection: NodeList) {.jsfin.} =
  collection.finalize0()

func ownerDocument(node: Node): Document {.jsfget.} =
  if node.nodeType == DOCUMENT_NODE:
    return nil
  return node.document

func hasChildNodes(node: Node): bool {.jsfget.} =
  return node.childList.len > 0

func len(collection: Collection): int =
  collection.refreshCollection()
  return collection.snapshot.len

func newCollection[T: Collection](root: Node, match: proc(node: Node): bool {.noSideEffect.}, islive: bool): T =
  result = T(
    islive: islive,
    match: match,
    root: root,
    id: root.document.colln
  )
  inc root.document.colln
  result.populateCollection()

func isElement(node: Node): bool =
  return node.nodeType == ELEMENT_NODE

func children*(node: Node): HTMLCollection {.jsfget.} =
  return newCollection[HTMLCollection](node, isElement, true)

func childNodes(node: Node): NodeList {.jsfget.} =
  return newCollection[NodeList](node, nil, true)

# DOMTokenList
func length(tokenList: DOMTokenList): int {.jsfget.} =
  return tokenList.toks.len

func item(tokenList: DOMTokenList, i: int): Option[string] {.jsfunc.} =
  if i < tokenList.toks.len:
    return some(tokenList.toks[i])

func contains*(tokenList: DOMTokenList, s: string): bool {.jsfunc.} =
  return s in tokenList.toks

proc update(tokenList: DOMTokenList) =
  if not tokenList.element.attrb(tokenList.localName) and tokenList.toks.len == 0:
    return
  tokenList.element.attr(tokenList.localName, tokenList.toks.join(' '))

proc add(tokenList: DOMTokenList, tokens: varargs[string]) {.jserr, jsfunc.} =
  for tok in tokens:
    if tok == "":
      #TODO should be DOMException
      JS_ERR JS_TypeError, "SyntaxError"
    if AsciiWhitespace in tok:
      #TODO should be DOMException
      JS_ERR JS_TypeError, "InvalidCharacterError"
  for tok in tokens:
    tokenList.toks.add(tok)
  tokenList.update()

proc remove(tokenList: DOMTokenList, tokens: varargs[string]) {.jserr, jsfunc.} =
  for tok in tokens:
    if tok == "":
      #TODO should be DOMException
      JS_ERR JS_TypeError, "SyntaxError"
    if AsciiWhitespace in tok:
      #TODO should be DOMException
      JS_ERR JS_TypeError, "InvalidCharacterError"
  for tok in tokens:
    let i = tokenList.toks.find(tok)
    if i != -1:
      tokenList.toks.delete(i)
  tokenList.update()

proc toggle(tokenList: DOMTokenList, token: string, force = none(bool)): bool {.jserr, jsfunc.} =
  if token == "":
    #TODO should be DOMException
    JS_ERR JS_TypeError, "SyntaxError"
  if AsciiWhitespace in token:
    #TODO should be DOMException
    JS_ERR JS_TypeError, "InvalidCharacterError"
  let i = tokenList.toks.find(token)
  if i != -1:
    if not force.get(false):
      tokenList.toks.delete(i)
      tokenList.update()
      return false
    return true
  if force.get(true):
    tokenList.toks.add(token)
    tokenList.update()
    return true
  return false

proc replace(tokenList: DOMTokenList, token, newToken: string): bool {.jserr, jsfunc.} =
  if token == "" or newToken == "":
    #TODO should be DOMException
    JS_ERR JS_TypeError, "SyntaxError"
  if AsciiWhitespace in token or AsciiWhitespace in newToken:
    #TODO should be DOMException
    JS_ERR JS_TypeError, "InvalidCharacterError"
  let i = tokenList.toks.find(token)
  if i == -1:
    return false
  tokenList.toks[i] = newToken
  tokenList.update()
  return true

const SupportedTokensMap = {
  "abcd": @["adsf"] #TODO
}.toTable()

func supports(tokenList: DOMTokenList, token: string): bool {.jserr, jsfunc.} =
  if tokenList.localName in SupportedTokensMap:
    let lowercase = token.toLowerAscii()
    return lowercase in SupportedTokensMap[tokenList.localName]
  else:
    JS_ERR JS_TypeError, "No supported tokens defined for attribute " & tokenList.localName

func `$`(tokenList: DOMTokenList): string {.jsfunc.} =
  return tokenList.toks.join(' ')

func value(tokenList: DOMTokenList): string {.jsfget.} =
  return $tokenList

func getter(tokenList: DOMTokenList, i: int): Option[string] {.jsgetprop.} =
  return tokenList.item(i)

# NodeList
func length(nodeList: NodeList): int {.jsfget.} =
  return nodeList.len

func hasprop(nodeList: NodeList, i: int): bool {.jshasprop.} =
  return i < nodeList.len

func item(nodeList: NodeList, i: int): Node {.jsfunc.} =
  if i < nodeList.len:
    return nodeList.snapshot[i]

func getter(nodeList: NodeList, i: int): Option[Node] {.jsgetprop.} =
  return option(nodeList.item(i))

# HTMLCollection
proc length(collection: HTMLCollection): int {.jsfget.} =
  return collection.len

func hasprop(collection: HTMLCollection, i: int): bool {.jshasprop.} =
  return i < collection.len

func item(collection: HTMLCollection, i: int): Element {.jsfunc.} =
  if i < collection.len:
    return Element(collection.snapshot[i])

func getter(collection: HTMLCollection, i: int): Option[Element] {.jsgetprop.} =
  return option(collection.item(i))

func newAttr(parent: Element, localName, value: string, prefix = "", namespaceURI = ""): Attr =
  return Attr(
    nodeType: ATTRIBUTE_NODE,
    document: parent.document,
    namespaceURI: namespaceURI,
    ownerElement: parent,
    localName: localName,
    prefix: prefix,
    value: value
  )

func name(attr: Attr): string {.jsfget.} =
  if attr.prefix == "":
    return attr.localName
  return attr.prefix & ':' & attr.localName

func findAttr(map: NamedNodeMap, name: string): int =
  for i in 0 ..< map.attrlist.len:
    if map.attrlist[i].name == name:
      return i
  return -1

func findAttrNS(map: NamedNodeMap, namespace, localName: string): int =
  for i in 0 ..< map.attrlist.len:
    if map.attrlist[i].namespaceURI == namespace and map.attrlist[i].localName == localName:
      return i
  return -1

func hasAttribute(element: Element, qualifiedName: string): bool {.jsfunc.} =
  let qualifiedName = if element.namespace == Namespace.HTML and not element.document.isxml:
    qualifiedName.toLowerAscii2()
  else:
    qualifiedName
  if qualifiedName in element.attrs:
    return true

func hasAttributeNS(element: Element, namespace, localName: string): bool {.jsfunc.} =
  return element.attributes.findAttrNS(namespace, localName) != -1

func getAttribute(element: Element, qualifiedName: string): Option[string] {.jsfunc.} =
  let qualifiedName = if element.namespace == Namespace.HTML and not element.document.isxml:
    qualifiedName.toLowerAscii2()
  else:
    qualifiedName
  element.attrs.withValue(qualifiedName, val):
    return some(val[])

func getAttributeNS(element: Element, namespace, localName: string): Option[string] {.jsfunc.} =
  let i = element.attributes.findAttrNS(namespace, localName)
  if i != -1:
    return some(element.attributes.attrlist[i].value)

func getNamedItem(map: NamedNodeMap, qualifiedName: string): Option[Attr] {.jsfunc.} =
  if map.element.hasAttribute(qualifiedName):
    let i = map.findAttr(qualifiedName)
    if i != -1:
      return some(map.attrlist[i])

func getNamedItemNS(map: NamedNodeMap, namespace, localName: string): Option[Attr] {.jsfunc.} =
  let i = map.findAttrNS(namespace, localName)
  if i != -1:
    return some(map.attrlist[i])

func length(map: NamedNodeMap): int {.jsfget.} =
  return map.element.attrs.len

func item(map: NamedNodeMap, i: int): Option[Attr] {.jsfunc.} =
  if i < map.attrlist.len:
    return some(map.attrlist[i])

func hasprop[T: int|string](map: NamedNodeMap, i: T): bool {.jshasprop.} =
  when T is int:
    return i < map.attrlist.len
  else:
    return map.getNamedItem(i).isSome

func getter[T: int|string](map: NamedNodeMap, i: T): Option[Attr] {.jsgetprop.} =
  when T is int:
    return map.item(i)
  else:
    return map.getNamedItem(i)

func length(characterData: CharacterData): int {.jsfget.} =
  return characterData.data.utf16Len

func scriptingEnabled*(element: Element): bool =
  if element.document == nil:
    return false
  if element.document.window == nil:
    return false
  return element.document.window.settings.scripting

func form*(element: FormAssociatedElement): HTMLFormElement =
  case element.tagType
  of TAG_INPUT: return HTMLInputElement(element).form
  of TAG_SELECT: return HTMLSelectElement(element).form
  of TAG_BUTTON: return HTMLButtonElement(element).form
  of TAG_TEXTAREA: return HTMLTextAreaElement(element).form
  else: assert false

func `form=`*(element: FormAssociatedElement, form: HTMLFormElement) =
  case element.tagType
  of TAG_INPUT: HTMLInputElement(element).form = form
  of TAG_SELECT:  HTMLSelectElement(element).form = form
  of TAG_BUTTON: HTMLButtonElement(element).form = form
  of TAG_TEXTAREA: HTMLTextAreaElement(element).form = form
  else: assert false

func canSubmitImplicitly*(form: HTMLFormElement): bool =
  const BlocksImplicitSubmission = {
    INPUT_TEXT, INPUT_SEARCH, INPUT_URL, INPUT_TEL, INPUT_EMAIL, INPUT_PASSWORD,
    INPUT_DATE, INPUT_MONTH, INPUT_WEEK, INPUT_TIME, INPUT_DATETIME_LOCAL,
    INPUT_NUMBER
  }
  var found = false
  for control in form.controls:
    if control.tagType == TAG_INPUT:
      let input = HTMLInputElement(control)
      if input.inputType in BlocksImplicitSubmission:
        if found:
          return false
        else:
          found = true
  return true

func qualifiedName*(element: Element): string =
  if element.namespacePrefix.issome: element.namespacePrefix.get & ':' & element.localName
  else: element.localName

func html*(document: Document): HTMLElement =
  for element in document.elements(TAG_HTML):
    return HTMLElement(element)

func head*(document: Document): HTMLElement =
  let html = document.html
  if html != nil:
    for element in html.elements(TAG_HEAD):
      return HTMLElement(element)

func body*(document: Document): HTMLElement =
  let html = document.html
  if html != nil:
    for element in html.elements(TAG_BODY):
      return HTMLElement(element)

func select*(option: HTMLOptionElement): HTMLSelectElement =
  for anc in option.ancestors:
    if anc.tagType == TAG_SELECT:
      return HTMLSelectElement(anc)
  return nil

func countChildren(node: Node, nodeType: NodeType): int =
  for child in node.childList:
    if child.nodeType == nodeType:
      inc result

func hasChild(node: Node, nodeType: NodeType): bool =
  for child in node.childList:
    if child.nodeType == nodeType:
      return false

func hasNextSibling(node: Node, nodeType: NodeType): bool =
  var node = node.nextSibling
  while node != nil:
    if node.nodeType == nodeType: return true
    node = node.nextSibling
  return false

func hasPreviousSibling(node: Node, nodeType: NodeType): bool =
  var node = node.previousSibling
  while node != nil:
    if node.nodeType == nodeType: return true
    node = node.previousSibling
  return false

func nodeValue(node: Node): Option[string] {.jsfget.} =
  case node.nodeType
  of CharacterDataNodes:
    return some(CharacterData(node).data)
  of ATTRIBUTE_NODE:
    return some(Attr(node).value)
  else: discard

func textContent*(node: Node): string {.jsfget.} =
  case node.nodeType
  of DOCUMENT_NODE, DOCUMENT_TYPE_NODE:
    return "" #TODO null
  of CharacterDataNodes:
    return CharacterData(node).data
  else:
    for child in node.childList:
      if child.nodeType != COMMENT_NODE:
        result &= child.textContent

func childTextContent*(node: Node): string =
  for child in node.childList:
    if child.nodeType == TEXT_NODE:
      result &= Text(child).data

func rootNode*(node: Node): Node =
  if node.root == nil: return node
  return node.root

func isConnected*(node: Node): bool {.jsfget.} =
  return node.rootNode.nodeType == DOCUMENT_NODE #TODO shadow root

func inSameTree*(a, b: Node): bool =
  a.rootNode == b.rootNode

# a == b or b in a's ancestors
func contains*(a, b: Node): bool =
  for node in a.branch:
    if node == b: return true
  return false

func firstChild*(node: Node): Node {.jsfget.} =
  if node.childList.len == 0:
    return nil
  return node.childList[0]

func lastChild*(node: Node): Node {.jsfget.} =
  if node.childList.len == 0:
    return nil
  return node.childList[^1]

func firstElementChild*(node: Node): Element {.jsfget.} =
  for child in node.elementList:
    return child
  return nil

func lastElementChild*(node: Node): Element {.jsfget.} =
  for child in node.elementList:
    return child
  return nil

func findAncestor*(node: Node, tagTypes: set[TagType]): Element =
  for element in node.ancestors:
    if element.tagType in tagTypes:
      return element
  return nil

func getElementById*(node: Node, id: string): Element {.jsfunc.} =
  if id.len == 0:
    return nil
  for child in node.elements:
    if child.id == id:
      return child

func getElementsByTag*(node: Node, tag: TagType): seq[Element] =
  for element in node.elements(tag):
    result.add(element)

func getElementsByTagName0(root: Node, tagName: string): HTMLCollection =
  if tagName == "*":
    return newCollection[HTMLCollection](root, func(node: Node): bool = node.isElement, true)
  let t = tagType(tagName)
  if t != TAG_UNKNOWN:
    return newCollection[HTMLCollection](root, func(node: Node): bool = node.isElement and Element(node).tagType == t, true)

func getElementsByTagName(document: Document, tagName: string): HTMLCollection {.jsfunc.} =
  return document.getElementsByTagName0(tagName)

func getElementsByTagName(element: Element, tagName: string): HTMLCollection {.jsfunc.} =
  return element.getElementsByTagName0(tagName)

func getElementsByClassName0(node: Node, classNames: string): HTMLCollection =
  var classes = classNames.split(AsciiWhitespace)
  let isquirks = node.document.mode == QUIRKS
  if isquirks:
    for i in 0 .. classes.high:
      classes[i].mtoLowerAscii()
  return newCollection[HTMLCollection](node,
    func(node: Node): bool =
      if node.nodeType == ELEMENT_NODE:
        if isquirks:
          var cl = Element(node).classList
          for i in 0 .. cl.toks.high:
            cl.toks[i].mtoLowerAscii()
          for class in classes:
            if class notin cl:
              return false
        else:
          for class in classes:
            if class notin Element(node).classList:
              return false
        return true, true)

func getElementsByClassName(document: Document, classNames: string): HTMLCollection {.jsfunc.} =
  return document.getElementsByClassName0(classNames)

func getElementsByClassName(element: Element, classNames: string): HTMLCollection {.jsfunc.} =
  return element.getElementsByClassName0(classNames)

func previousElementSibling*(elem: Element): Element {.jsfget.} =
  if elem.parentNode == nil: return nil
  var i = elem.parentNode.childList.find(elem)
  dec i
  while i >= 0:
    if elem.parentNode.childList[i].nodeType == ELEMENT_NODE:
      return elem
    dec i
  return nil

func nextElementSibling*(elem: Element): Element {.jsfget.} =
  if elem.parentNode == nil: return nil
  var i = elem.parentNode.childList.find(elem)
  inc i
  while i < elem.parentNode.childList.len:
    if elem.parentNode.childList[i].nodeType == ELEMENT_NODE:
      return elem
    inc i
  return nil

func documentElement(document: Document): Element {.jsfget.} =
  document.firstElementChild()

func attr*(element: Element, s: string): string {.inline.} =
  return element.attrs.getOrDefault(s, "")

func attri*(element: Element, s: string): Option[int] =
  let a = element.attr(s)
  try:
    return some(parseInt(a))
  except ValueError:
    return none(int)

func attrigz*(element: Element, s: string): Option[int] =
  let a = element.attr(s)
  try:
    let i = parseInt(a)
    if i > 0:
      return some(i)
  except ValueError:
    discard

func attrigez*(element: Element, s: string): Option[int] =
  let a = element.attr(s)
  try:
    let i = parseInt(a)
    if i >= 0:
      return some(i)
  except ValueError:
    discard

func attrb*(element: Element, s: string): bool =
  if s in element.attrs:
    return true
  return false

# Element attribute reflection (getters)
func innerHTML*(element: Element): string {.jsfget.} =
  for child in element.childList:
    result &= $child

func outerHTML*(element: Element): string {.jsfget.} =
  return $element

func crossorigin(element: HTMLScriptElement): CORSAttribute =
  if not element.attrb("crossorigin"):
    return NO_CORS
  case element.attr("crossorigin")
  of "anonymous", "":
    return ANONYMOUS
  of "use-credentials":
    return USE_CREDENTIALS
  return ANONYMOUS

func referrerpolicy(element: HTMLScriptElement): Option[ReferrerPolicy] =
  getReferrerPolicy(element.attr("referrerpolicy"))

proc sheets*(document: Document): seq[CSSStylesheet] =
  if document.cachedSheetsInvalid:
    document.cachedSheets.setLen(0)
    for elem in document.html.elements({TAG_STYLE, TAG_LINK}):
      case elem.tagType
      of TAG_STYLE:
        let style = HTMLStyleElement(elem)
        style.sheet = parseStylesheet(newStringStream(style.textContent))
        if style.sheet != nil:
          document.cachedSheets.add(style.sheet)
      of TAG_LINK:
        let link = HTMLLinkElement(elem)
        if link.sheet != nil:
          document.cachedSheets.add(link.sheet)
      else: discard
    document.cachedSheetsInvalid = false
  return document.cachedSheets

func inputString*(input: HTMLInputElement): string =
  case input.inputType
  of INPUT_CHECKBOX, INPUT_RADIO:
    if input.checked: "*"
    else: " "
  of INPUT_SEARCH, INPUT_TEXT:
    input.value.padToWidth(input.attri("size").get(20))
  of INPUT_PASSWORD:
    '*'.repeat(input.value.len).padToWidth(input.attri("size").get(20))
  of INPUT_RESET:
    if input.value != "": input.value
    else: "RESET"
  of INPUT_SUBMIT, INPUT_BUTTON:
    if input.value != "": input.value
    else: "SUBMIT"
  of INPUT_FILE:
    if input.file.isnone:
      "".padToWidth(input.attri("size").get(20))
    else:
      input.file.get.path.serialize_unicode().padToWidth(input.attri("size").get(20))
  else: input.value

func textAreaString*(textarea: HTMLTextAreaElement): string =
  let split = textarea.value.split('\n')
  let rows = textarea.attri("rows").get(1)
  for i in 0 ..< rows:
    let cols = textarea.attri("cols").get(20)
    if cols > 2:
      if i < split.len:
        result &= '[' & split[i].padToWidth(cols - 2) & "]\n"
      else:
        result &= '[' & ' '.repeat(cols - 2) & "]\n"
    else:
      result &= "[]\n"

func isButton*(element: Element): bool =
  if element.tagType == TAG_BUTTON:
    return true
  if element.tagType == TAG_INPUT:
    let element = HTMLInputElement(element)
    return element.inputType in {INPUT_SUBMIT, INPUT_BUTTON, INPUT_RESET, INPUT_IMAGE}
  return false

func isSubmitButton*(element: Element): bool =
  if element.tagType == TAG_BUTTON:
    return element.attr("type") == "submit"
  elif element.tagType == TAG_INPUT:
    let element = HTMLInputElement(element)
    return element.inputType in {INPUT_SUBMIT, INPUT_IMAGE}
  return false

func action*(element: Element): string =
  if element.isSubmitButton():
    if element.attrb("formaction"):
      return element.attr("formaction")
  if element.tagType == TAG_INPUT:
    let element = HTMLInputElement(element)
    if element.form != nil:
      if element.form.attrb("action"):
        return element.form.attr("action")
  if element.tagType == TAG_FORM:
    return element.attr("action")
  return ""

func enctype*(element: Element): FormEncodingType =
  if element.isSubmitButton():
    if element.attrb("formenctype"):
      return case element.attr("formenctype").tolower()
      of "application/x-www-form-urlencoded": FORM_ENCODING_TYPE_URLENCODED
      of "multipart/form-data": FORM_ENCODING_TYPE_MULTIPART
      of "text/plain": FORM_ENCODING_TYPE_TEXT_PLAIN
      else: FORM_ENCODING_TYPE_URLENCODED

  if element.tagType == TAG_INPUT:
    let element = HTMLInputElement(element)
    if element.form != nil:
      if element.form.attrb("enctype"):
        return case element.attr("enctype").tolower()
        of "application/x-www-form-urlencoded": FORM_ENCODING_TYPE_URLENCODED
        of "multipart/form-data": FORM_ENCODING_TYPE_MULTIPART
        of "text/plain": FORM_ENCODING_TYPE_TEXT_PLAIN
        else: FORM_ENCODING_TYPE_URLENCODED

  return FORM_ENCODING_TYPE_URLENCODED

func formmethod*(element: Element): FormMethod =
  if element.isSubmitButton():
    if element.attrb("formmethod"):
      return case element.attr("formmethod").tolower()
      of "get": FORM_METHOD_GET
      of "post": FORM_METHOD_POST
      of "dialog": FORM_METHOD_DIALOG
      else: FORM_METHOD_GET

  if element.tagType in SupportedFormAssociatedElements:
    let element = FormAssociatedElement(element)
    if element.form != nil:
      if element.form.attrb("method"):
        return case element.form.attr("method").tolower()
        of "get": FORM_METHOD_GET
        of "post": FORM_METHOD_POST
        of "dialog": FORM_METHOD_DIALOG
        else: FORM_METHOD_GET

  return FORM_METHOD_GET

#TODO ??
func target0*(element: Element): string =
  if element.attrb("target"):
    return element.attr("target")
  for base in element.document.elements(TAG_BASE):
    if base.attrb("target"):
      return base.attr("target")
  return ""

# HTMLHyperlinkElementUtils (for <a> and <area>)
func href0[T: HTMLAnchorElement|HTMLAreaElement](element: T): string =
  if element.attrb("href"):
    let url = parseUrl(element.attr("href"), some(element.document.url))
    if url.issome:
      return $url.get

# <base>
func href(base: HTMLBaseElement): string {.jsfget.} =
  if base.attrb("href"):
    #TODO with fallback base url
    let url = parseUrl(base.attr("href"))
    if url.isSome:
      return $url.get

# <a>
func href*(anchor: HTMLAnchorElement): string {.jsfget.} =
  anchor.href0

proc href(anchor: HTMLAnchorElement, href: string) {.jsfset.} =
  anchor.attr("href", href)

func `$`(anchor: HTMLAnchorElement): string {.jsfunc.} =
  anchor.href

# <area>
func href(area: HTMLAreaElement): string {.jsfget.} =
  area.href0

proc href(area: HTMLAreaElement, href: string) {.jsfset.} =
  area.attr("href", href)

func `$`(area: HTMLAreaElement): string {.jsfunc.} =
  area.href

# <label>
func control*(label: HTMLLabelElement): FormAssociatedElement {.jsfget.} =
  let f = label.attr("for")
  if f != "":
    let elem = label.document.getElementById(f)
    #TODO the supported check shouldn't be needed, just labelable
    if elem.tagType in SupportedFormAssociatedElements and elem.tagType in LabelableElements:
      return FormAssociatedElement(elem)
    return nil
  for elem in label.elements(LabelableElements):
    if elem.tagType in SupportedFormAssociatedElements: #TODO remove this
      return FormAssociatedElement(elem)
    return nil

func form(label: HTMLLabelElement): HTMLFormElement {.jsfget.} =
  let control = label.control
  if control != nil:
    return control.form

func newText(document: Document, data: string): Text =
  return Text(
    nodeType: TEXT_NODE,
    document: document,
    data: data
  )

func newText(window: Window, data: string = ""): Text {.jsgctor.} =
  return window.document.newText(data)

func newCDATASection(document: Document, data: string): CDATASection =
  return CDATASection(
    nodeType: CDATA_SECTION_NODE,
    document: document,
    data: data
  )

func newProcessingInstruction(document: Document, target, data: string): ProcessingInstruction =
  return ProcessingInstruction(
    nodeType: PROCESSING_INSTRUCTION_NODE,
    document: document,
    target: target,
    data: data
  )

func newDocumentFragment(document: Document): DocumentFragment =
  return DocumentFragment(
    nodeType: DOCUMENT_FRAGMENT_NODE,
    document: document
  )

func newDocumentFragment(window: Window): DocumentFragment {.jsgctor.} =
  return window.document.newDocumentFragment()

func newComment(document: Document, data: string): Comment =
  return Comment(
    nodeType: COMMENT_NODE,
    document: document,
    data: data
  )

func newComment(window: Window, data: string = ""): Comment {.jsgctor.} =
  return window.document.newComment(data)

#TODO custom elements
func newHTMLElement*(document: Document, tagType: TagType, namespace = Namespace.HTML, prefix = none[string](), attrs = Table[string, string]()): HTMLElement =
  case tagType
  of TAG_INPUT:
    result = new(HTMLInputElement)
  of TAG_A:
    result = new(HTMLAnchorElement)
  of TAG_SELECT:
    result = new(HTMLSelectElement)
  of TAG_OPTGROUP:
    result = new(HTMLOptGroupElement)
  of TAG_OPTION:
    result = new(HTMLOptionElement)
  of TAG_H1, TAG_H2, TAG_H3, TAG_H4, TAG_H5, TAG_H6:
    result = new(HTMLHeadingElement)
  of TAG_BR:
    result = new(HTMLBRElement)
  of TAG_SPAN:
    result = new(HTMLSpanElement)
  of TAG_OL:
    result = new(HTMLOListElement)
  of TAG_UL:
    result = new(HTMLUListElement)
  of TAG_MENU:
    result = new(HTMLMenuElement)
  of TAG_LI:
    result = new(HTMLLIElement)
  of TAG_STYLE:
    result = new(HTMLStyleElement)
  of TAG_LINK:
    result = new(HTMLLinkElement)
  of TAG_FORM:
    result = new(HTMLFormElement)
  of TAG_TEMPLATE:
    result = new(HTMLTemplateElement)
    HTMLTemplateElement(result).content = DocumentFragment(document: document, host: result)
  of TAG_UNKNOWN:
    result = new(HTMLUnknownElement)
  of TAG_SCRIPT:
    result = new(HTMLScriptElement)
    HTMLScriptElement(result).forceAsync = true
  of TAG_BASE:
    result = new(HTMLBaseElement)
  of TAG_BUTTON:
    result = new(HTMLButtonElement)
  of TAG_TEXTAREA:
    result = new(HTMLTextAreaElement)
  of TAG_LABEL:
    result = new(HTMLLabelElement)
  else:
    result = new(HTMLElement)
  result.nodeType = ELEMENT_NODE
  result.tagType = tagType
  result.namespace = namespace
  result.namespacePrefix = prefix
  result.document = document
  result.attributes = NamedNodeMap(element: result)
  result.classList = DOMTokenList(localName: "classList")
  {.cast(noSideEffect).}:
    for k, v in attrs:
      result.attr(k, v)
  if tagType == TAG_SCRIPT:
    HTMLScriptElement(result).internalNonce = result.attr("nonce")

func newHTMLElement*(document: Document, localName: string, namespace = Namespace.HTML, prefix = none[string](), tagType = tagType(localName), attrs = Table[string, string]()): Element =
  result = document.newHTMLElement(tagType, namespace, prefix, attrs)
  if tagType == TAG_UNKNOWN:
    result.localName = localName

func newDocument*(): Document {.jsctor.} =
  result = Document(
    nodeType: DOCUMENT_NODE
  )
  result.document = result
  result.implementation = DOMImplementation(document: result)
  result.contentType = "application/xml"

func newDocumentType*(document: Document, name: string, publicId = "", systemId = ""): DocumentType =
  return DocumentType(
    nodeType: DOCUMENT_TYPE_NODE,
    document: document,
    name: name,
    publicId: publicId,
    systemId: systemId
  )

func inHTMLNamespace*(element: Element): bool = element.namespace == Namespace.HTML
func inMathMLNamespace*(element: Element): bool = element.namespace == Namespace.MATHML
func inSVGNamespace*(element: Element): bool = element.namespace == Namespace.SVG
func inXLinkNamespace*(element: Element): bool = element.namespace == Namespace.XLINK
func inXMLNamespace*(element: Element): bool = element.namespace == Namespace.XML
func inXMLNSNamespace*(element: Element): bool = element.namespace == Namespace.XMLNS

func isResettable*(element: Element): bool =
  return element.tagType in {TAG_INPUT, TAG_OUTPUT, TAG_SELECT, TAG_TEXTAREA}

func isHostIncludingInclusiveAncestor*(a, b: Node): bool =
  for parent in b.branch:
    if parent == a:
      return true
  if b.rootNode.nodeType == DOCUMENT_FRAGMENT_NODE and DocumentFragment(b.rootNode).host != nil:
    for parent in b.rootNode.branch:
      if parent == a:
        return true
  return false

func baseURL*(document: Document): Url =
  #TODO frozen base url...
  var href = ""
  for base in document.elements(TAG_BASE):
    if base.attrb("href"):
      href = base.attr("href")
  if href == "":
    return document.url
  if document.url == nil:
    return newURL("about:blank") #TODO ???
  let url = parseURL(href, some(document.url))
  if url.isNone:
    return document.url
  return url.get

func parseURL*(document: Document, s: string): Option[URL] =
  #TODO encodings
  return parseURL(s, some(document.baseURL))

func rel*[T: HTMLAnchorElement|HTMLLinkElement|HTMLAreaElement](element: T): string =
  return element.attr("rel")

func media*[T: HTMLLinkElement|HTMLStyleElement](element: T): string =
  return element.attr("media")

func title*(document: Document): string =
  for title in document.elements(TAG_TITLE):
    return title.childTextContent.stripAndCollapse()
  return ""

func disabled*(option: HTMLOptionElement): bool =
  if option.parentElement.tagType == TAG_OPTGROUP and option.parentElement.attrb("disabled"):
    return true
  return option.attrb("disabled")

func text*(option: HTMLOptionElement): string =
  for child in option.descendants:
    if child.nodeType == TEXT_NODE:
      let child = Text(child)
      if child.parentElement.tagType != TAG_SCRIPT: #TODO svg
        result &= child.data.stripAndCollapse()

func value*(option: HTMLOptionElement): string {.jsfget.} =
  if option.attrb("value"):
    return option.attr("value")
  return option.childTextContent.stripAndCollapse()

proc invalidateCollections(node: Node) =
  for id in node.liveCollections:
    node.document.invalidCollections.incl(id)

proc delAttr(element: Element, i: int) =
  if i != -1:
    let attr = element.attributes.attrlist[i]
    element.attrs.del(attr.name)
    element.attributes.attrlist.delete(i)
    element.invalidateCollections()
    element.invalid = true

proc delAttr(element: Element, name: string) =
  let i = element.attributes.findAttr(name)
  if i != -1:
    element.delAttr(i)

proc reflectAttrs(element: Element, name, value: string) =
  template reflect_str(element: Element, n: static string, val: untyped) =
    if name == n:
      element.val = value
      return
  template reflect_str(element: Element, n: static string, val, fun: untyped) =
    if name == n:
      element.val = fun(value)
  template reflect_bool(element: Element, name: static string, val: untyped) =
    if name in element.attrs:
      element.val = true
  element.reflect_str "id", id
  if name == "class":
    element.classList.toks.setLen(0)
    for x in value.split(AsciiWhitespace):
      if x != "" and x notin element.classList:
        element.classList.toks.add(x)
    return
  case element.tagType
  of TAG_INPUT:
    let input = HTMLInputElement(element)
    input.reflect_str "value", value
    input.reflect_str "type", inputType, inputType
    input.reflect_bool "checked", checked
  of TAG_OPTION:
    let option = HTMLOptionElement(element)
    option.reflect_bool "selected", selected
  of TAG_BUTTON:
    let button = HTMLButtonElement(element)
    button.reflect_str "type", ctype, (func(s: string): ButtonType =
      case s
      of "submit": return BUTTON_SUBMIT
      of "reset": return BUTTON_RESET
      of "button": return BUTTON_BUTTON)
  else: discard

proc attr0(element: Element, name, value: string) =
  element.attrs.withValue(name, val):
    val[] = value
    element.invalidateCollections()
    element.invalid = true
  do: # else
    element.attrs[name] = value
  element.reflectAttrs(name, value)

proc attr*(element: Element, name, value: string) =
  let i = element.attributes.findAttr(name)
  if i != -1:
    element.attributes.attrlist[i].value = value
  else:
    element.attributes.attrlist.add(element.newAttr(name, value))
  element.attr0(name, value)

proc attri(element: Element, name: string, value: int) =
  element.attr(name, $value)

proc attrigz(element: Element, name: string, value: int) =
  if value > 0:
    element.attri(name, value)

proc attrigez(element: Element, name: string, value: int) =
  if value >= 0:
    element.attri(name, value)

proc setAttribute(element: Element, qualifiedName, value: string) {.jserr, jsfunc.} =
  if not qualifiedName.matchNameProduction():
    #TODO should be DOMException
    JS_ERR JS_TypeError, "InvalidCharacterError"
  let qualifiedName = if element.namespace == Namespace.HTML and not element.document.isxml:
    qualifiedName.toLowerAscii2()
  else:
    qualifiedName
  element.attr(qualifiedName, value)

proc setAttributeNS(element: Element, namespace, qualifiedName, value: string) {.jserr, jsfunc.} =
  if not qualifiedName.matchQNameProduction():
    #TODO this should be a DOMException
    JS_ERR JS_TypeError, "InvalidCharacterError"
  let ps = qualifiedName.until(':')
  let prefix = if ps.len < qualifiedName.len: ps else: ""
  let localName = qualifiedName.substr(prefix.len)
  if prefix != "" and namespace == "" or
      prefix == "xml" and namespace != $Namespace.XML or
      (qualifiedName == "xmlns" or prefix == "xmlns") and namespace != $Namespace.XMLNS or
      namespace == $Namespace.XMLNS and qualifiedName != "xmlns" and prefix != "xmlns":
    #TODO this should be a DOMException
    JS_ERR JS_TypeError, "NamespaceError"
  element.attr0(qualifiedName, value)
  let i = element.attributes.findAttrNS(namespace, localName)
  if i != -1:
    element.attributes.attrlist[i].value = value
  else:
    element.attributes.attrlist.add(element.newAttr(localName, value, prefix, namespace))

proc removeAttribute(element: Element, qualifiedName: string) {.jsfunc.} =
  let qualifiedName = if element.namespace == Namespace.HTML and not element.document.isxml:
    qualifiedName.toLowerAscii2()
  else:
    qualifiedName
  element.delAttr(qualifiedName)

proc removeAttributeNS(element: Element, namespace, localName: string) {.jsfunc.} =
  let i = element.attributes.findAttrNS(namespace, localName)
  if i != -1:
    element.delAttr(i)

proc toggleAttribute(element: Element, qualifiedName: string, force = none(bool)): bool {.jserr, jsfunc.} =
  if not qualifiedName.matchNameProduction():
    #TODO should be DOMException
    JS_ERR JS_TypeError, "InvalidCharacterError"
  let qualifiedName = if element.namespace == Namespace.HTML and not element.document.isxml:
    qualifiedName.toLowerAscii2()
  else:
    qualifiedName
  if not element.attrb(qualifiedName):
    if force.get(true):
      element.attr(qualifiedName, "")
      return true
    return false
  if not force.get(false):
    element.delAttr(qualifiedName)
    return false
  return true

proc value(attr: Attr, s: string) {.jsfset.} =
  attr.value = s
  if attr.ownerElement != nil:
    attr.ownerElement.attr0(attr.name, s)

proc setNamedItem(map: NamedNodeMap, attr: Attr): Option[Attr] {.jserr, jsfunc.} =
  if attr.ownerElement != nil and attr.ownerElement != map.element:
    #TODO should be DOMException
    JS_ERR JS_TypeError, "InUseAttributeError"
  if attr.name in map.element.attrs:
    return some(attr)
  let i = map.findAttr(attr.name)
  if i != -1:
    result = some(map.attrlist[i])
    map.attrlist.delete(i)
  map.element.attrs[attr.name] = attr.value
  map.attrlist.add(attr)

proc setNamedItemNS(map: NamedNodeMap, attr: Attr): Option[Attr] {.jsfunc.} =
  map.setNamedItem(attr)

proc removeNamedItem(map: NamedNodeMap, qualifiedName: string): Attr {.jserr, jsfunc.} =
  let i = map.findAttr(qualifiedName)
  if i != -1:
    let attr = map.attrlist[i]
    map.element.delAttr(i)
    return attr
  #TODO should be DOMException
  JS_ERR JS_TypeError, "Not found"

proc removeNamedItemNS(map: NamedNodeMap, namespace, localName: string): Attr {.jserr, jsfunc.} =
  let i = map.findAttrNS(namespace, localName)
  if i != -1:
    let attr = map.attrlist[i]
    map.element.delAttr(i)
    return attr
  #TODO should be DOMException
  JS_ERR JS_TypeError, "Not found"

proc id(element: Element, id: string) {.jsfset.} =
  element.id = id
  element.attr("id", id)

# Pass an index to avoid searching for the node in parent's child list.
proc remove*(node: Node, index: int, suppressObservers: bool) =
  let parent = node.parentNode
  assert parent != nil
  assert index != -1
  #TODO live ranges
  #TODO NodeIterator
  let oldPreviousSibling = node.previousSibling
  let oldNextSibling = node.nextSibling
  parent.childList.delete(index)
  if oldPreviousSibling != nil:
    oldPreviousSibling.nextSibling = oldNextSibling
  if oldNextSibling != nil:
    oldNextSibling.previousSibling = oldPreviousSibling
  node.parentNode.invalidateCollections()
  node.parentNode = nil
  node.parentElement = nil
  node.root = nil
  if node.nodeType == ELEMENT_NODE:
    if Element(node).tagType in {TAG_STYLE, TAG_LINK} and node.document != nil:
      node.document.cachedSheetsInvalid = true

  #TODO assigned, shadow root, shadow root again, custom nodes, registered observers
  #TODO not suppress observers => queue tree mutation record

proc remove*(node: Node, suppressObservers = false) =
  let index = node.parentNode.childList.find(node)
  node.remove(index, suppressObservers)

proc adopt(document: Document, node: Node) =
  let oldDocument = node.document
  if node.parentNode != nil:
    remove(node)
  if oldDocument != document:
    #TODO shadow root
    for desc in node.descendants:
      desc.document = document
      if desc.nodeType == ELEMENT_NODE:
        for attr in Element(desc).attributes.attrlist:
          attr.document = document
    #TODO custom elements
    #..adopting steps

proc applyChildInsert(parent, child: Node, index: int) =
  child.root = parent.rootNode
  child.parentNode = parent
  if parent.nodeType == ELEMENT_NODE:
    child.parentElement = Element(parent)
  if index - 1 >= 0:
    child.previousSibling = parent.childList[index - 1]
    child.previousSibling.nextSibling = child
  if index + 1 < parent.childList.len:
    child.nextSibling = parent.childList[index + 1]
    child.nextSibling.previousSibling = child
  child.invalidateCollections()
  if child.nodeType == ELEMENT_NODE:
    if Element(child).tagType in {TAG_STYLE, TAG_LINK} and child.document != nil:
      child.document.cachedSheetsInvalid = true

proc resetElement*(element: Element) = 
  case element.tagType
  of TAG_INPUT:
    let input = HTMLInputELement(element)
    case input.inputType
    of INPUT_SEARCH, INPUT_TEXT, INPUT_PASSWORD:
      input.value = input.attr("value")
    of INPUT_CHECKBOX, INPUT_RADIO:
      input.checked = input.attrb("checked")
    of INPUT_FILE:
      input.file = none(Url)
    else: discard
    input.invalid = true
  of TAG_SELECT:
    let select = HTMLSelectElement(element)
    if not select.attrb("multiple"):
      if select.attrigez("size").get(1) == 1:
        var i = 0
        var firstOption: HTMLOptionElement
        for option in select.options:
          if firstOption == nil:
            firstOption = option
          if option.selected:
            inc i
        if i == 0 and firstOption != nil:
          firstOption.selected = true
        elif i > 2:
          # Set the selectedness of all but the last selected option element to
          # false.
          var j = 0
          for option in select.options:
            if j == i: break
            if option.selected:
              option.selected = false
              inc j
  of TAG_TEXTAREA:
    let textarea = HTMLTextAreaElement(element)
    textarea.value = textarea.childTextContent()
    textarea.invalid = true
  else: discard

proc setForm*(element: FormAssociatedElement, form: HTMLFormElement) =
  case element.tagType
  of TAG_INPUT:
    let input = HTMLInputElement(element)
    input.form = form
    form.controls.add(input)
  of TAG_SELECT:
    let select = HTMLSelectElement(element)
    select.form = form
    form.controls.add(select)
  of TAG_BUTTON:
    let button = HTMLButtonElement(element)
    button.form = form
    form.controls.add(button)
  of TAG_TEXTAREA:
    let textarea = HTMLTextAreaElement(element)
    textarea.form = form
    form.controls.add(textarea)
  of TAG_FIELDSET, TAG_OBJECT, TAG_OUTPUT, TAG_IMG:
    discard #TODO
  else: assert false

proc resetFormOwner(element: FormAssociatedElement) =
  element.parserInserted = false
  if element.form != nil and
      element.tagType notin ListedElements or not element.attrb("form") and
      element.findAncestor({TAG_FORM}) == element.form:
    return
  element.form = nil
  if element.tagType in ListedElements and element.attrb("form") and element.isConnected:
    let form = element.attr("form")
    for desc in element.elements(TAG_FORM):
      if desc.id == form:
        element.setForm(HTMLFormElement(desc))

proc insertionSteps(insertedNode: Node) =
  if insertedNode.nodeType == ELEMENT_NODE:
    let element = Element(insertedNode)
    let tagType = element.tagType
    case tagType
    of TAG_OPTION:
      if element.parentElement != nil:
        let parent = element.parentElement
        var select: HTMLSelectElement
        if parent.tagType == TAG_SELECT:
          select = HTMLSelectElement(parent)
        elif parent.tagType == TAG_OPTGROUP and parent.parentElement != nil and parent.parentElement.tagType == TAG_SELECT:
          select = HTMLSelectElement(parent.parentElement)
        if select != nil:
          select.resetElement()
    else: discard
    if tagType in SupportedFormAssociatedElements:
      let element = FormAssociatedElement(element)
      if element.parserInserted:
        return
      element.resetFormOwner()

# WARNING the ordering of the arguments in the standard is whack so this doesn't match that
func preInsertionValidity*(parent, node, before: Node): bool =
  if parent.nodeType notin {DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE}:
    # HierarchyRequestError
    return false
  if node.isHostIncludingInclusiveAncestor(parent):
    # HierarchyRequestError
    return false
  if before != nil and before.parentNode != parent:
    # NotFoundError
    return false
  if node.nodeType notin {DOCUMENT_FRAGMENT_NODE, DOCUMENT_TYPE_NODE, ELEMENT_NODE} + CharacterDataNodes:
    # HierarchyRequestError
    return false
  if (node.nodeType == TEXT_NODE and parent.nodeType == DOCUMENT_NODE) or
      (node.nodeType == DOCUMENT_TYPE_NODE and parent.nodeType != DOCUMENT_NODE):
    # HierarchyRequestError
    return false
  if parent.nodeType == DOCUMENT_NODE:
    case node.nodeType
    of DOCUMENT_FRAGMENT_NODE:
      let elems = node.countChildren(ELEMENT_NODE)
      if elems > 1 or node.hasChild(TEXT_NODE):
        # HierarchyRequestError
        return false
      elif elems == 1 and (parent.hasChild(ELEMENT_NODE) or before != nil and (before.nodeType == DOCUMENT_TYPE_NODE or before.hasNextSibling(DOCUMENT_TYPE_NODE))):
        # HierarchyRequestError
        return false
    of ELEMENT_NODE:
      if parent.hasChild(ELEMENT_NODE) or before != nil and (before.nodeType == DOCUMENT_TYPE_NODE or before.hasNextSibling(DOCUMENT_TYPE_NODE)):
        # HierarchyRequestError
        return false
    of DOCUMENT_TYPE_NODE:
      if parent.hasChild(DOCUMENT_TYPE_NODE) or before != nil and before.hasPreviousSibling(ELEMENT_NODE) or before == nil and parent.hasChild(ELEMENT_NODE):
        # HierarchyRequestError
        return false
    else: discard
  return true # no exception reached

# WARNING ditto
proc insert*(parent, node, before: Node) =
  let nodes = if node.nodeType == DOCUMENT_FRAGMENT_NODE: node.childList
  else: @[node]
  let count = nodes.len
  if count == 0:
    return
  if node.nodeType == DOCUMENT_FRAGMENT_NODE:
    for i in countdown(node.childList.high, 0):
      node.childList[i].remove(i, true)
    #TODO tree mutation record
  if before != nil:
    #TODO live ranges
    discard
  if parent.nodeType == ELEMENT_NODE:
    Element(parent).invalid = true
  for node in nodes:
    parent.document.adopt(node)
    if before == nil:
      parent.childList.add(node)
      parent.applyChildInsert(node, parent.childList.high)
    else:
      let index = parent.childList.find(before)
      parent.childList.insert(node, index)
      parent.applyChildInsert(node, index)
    if node.nodeType == ELEMENT_NODE:
      #TODO shadow root
      insertionSteps(node)

proc insertBefore(parent, node, before: Node): Node {.jserr, jsfunc.} =
  if parent.preInsertionValidity(node, before):
    let referenceChild = if before == node:
      node.nextSibling
    else:
      before
    parent.insert(node, referenceChild)
    return node
  #TODO use preInsertionValidity result
  JS_ERR JS_TypeError, "Pre-insertion validity violated"

proc appendChild(parent, node: Node): Node {.jsfunc.} =
  return parent.insertBefore(node, nil)

proc append*(parent, node: Node) =
  discard parent.appendChild(node)

#TODO replaceChild

proc removeChild(parent, node: Node): Node {.jsfunc.} =
  #TODO should be DOMException
  if node.parentNode != parent:
    JS_ERR JS_TypeError, "NotFoundError"
  node.remove()

proc replaceAll(parent, node: Node) =
  for i in countdown(parent.childList.high, 0):
    parent.childList[i].remove(i, true)
  if node != nil:
    if node.nodeType == DOCUMENT_FRAGMENT_NODE:
      for child in node.childList:
        parent.append(child)
    else:
      parent.append(node)
  #TODO tree mutation record

proc createTextNode*(document: Document, data: string): Text {.jsfunc.} =
  return newText(document, data)

proc textContent*(node: Node, data: Option[string]) {.jsfset.} =
  case node.nodeType
  of DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE:
    let x = if data.isSome:
      node.document.createTextNode(data.get)
    else:
      nil
    node.replaceAll(x)
  of ATTRIBUTE_NODE:
    value(Attr(node), data.get(""))
  of TEXT_NODE, COMMENT_NODE:
    CharacterData(node).data = data.get("")
  else: discard

proc reset*(form: HTMLFormElement) =
  for control in form.controls:
    control.resetElement()
    control.invalid = true

proc renderBlocking*(element: Element): bool =
  if "render" in element.attr("blocking").split(AsciiWhitespace):
    return true
  if element.tagType == TAG_SCRIPT:
    let element = HTMLScriptElement(element)
    if element.ctype == CLASSIC and element.parserDocument != nil and
        not element.attrb("async") and not element.attrb("defer"):
      return true
  return false

proc blockRendering*(element: Element) =
  let document = element.document
  if document != nil and document.contentType == "text/html" and document.body == nil:
    element.document.renderBlockingElements.add(element)

proc markAsReady(element: HTMLScriptElement, res: ScriptResult) =
  element.scriptResult = res
  if element.onReady != nil:
    element.onReady()
    element.onReady = nil
  element.delayingTheLoadEvent = false

proc createClassicScript(source: string, baseURL: URL, options: ScriptOptions, mutedErrors = false): Script =
  return Script(
    record: source,
    baseURL: baseURL,
    options: options,
    mutedErrors: mutedErrors
  )

#TODO settings object
proc fetchClassicScript(element: HTMLScriptElement, url: URL,
                        options: ScriptOptions, cors: CORSAttribute,
                        cs: Charset, onComplete: (proc(element: HTMLScriptElement,
                                                       res: ScriptResult))) =
  if not element.scriptingEnabled:
      element.onComplete(ScriptResult(t: RESULT_NULL))
  else:
    let loader = element.document.window.loader
    if loader.isSome:
      let request = createPotentialCORSRequest(url, RequestDestination.SCRIPT, cors)
      #TODO this should be async...
      let r = loader.get.doRequest(request)
      if r.res != 0 or r.body == nil:
        element.onComplete(ScriptResult(t: RESULT_NULL))
      else:
        #TODO use charset from content-type
        let cs = if cs == CHARSET_UNKNOWN: CHARSET_UTF_8 else: cs
        let source = newDecoderStream(r.body, cs = cs).readAll()
        #TODO use response url
        let script = createClassicScript(source, url, options, false)
        element.markAsReady(ScriptResult(t: RESULT_SCRIPT, script: script))

proc log*(console: console, ss: varargs[string]) {.jsfunc.} =
  var s = ""
  for i in 0..<ss.len:
    s &= ss[i]
    console.err.write(ss[i])
    if i != ss.high:
      s &= ' '
      console.err.write(' ')
  console.err.write('\n')
  console.err.flush()

proc execute*(element: HTMLScriptElement) =
  let document = element.document
  if document != element.preparationTimeDocument:
    return
  let i = document.renderBlockingElements.find(element)
  if i != -1:
    document.renderBlockingElements.delete(i)
  if element.scriptResult.t == RESULT_NULL:
    #TODO fire error event
    return
  case element.ctype
  of CLASSIC:
    let oldCurrentScript = document.currentScript
    #TODO not if shadow root
    document.currentScript = element
    if document.window != nil and document.window.jsctx != nil:
      let script = element.scriptResult.script
      let ret = document.window.jsctx.eval(script.record, $script.baseURL, JS_EVAL_TYPE_GLOBAL)
      if JS_IsException(ret):
        let ss = newStringStream()
        document.window.jsctx.writeException(ss)
        ss.setPosition(0)
        document.window.console.log("Exception in document", $document.url, ss.readAll())
    document.currentScript = oldCurrentScript
  else: discard #TODO

# https://html.spec.whatwg.org/multipage/scripting.html#prepare-the-script-element
proc prepare*(element: HTMLScriptElement) =
  if element.alreadyStarted:
    return
  let parserDocument = element.parserDocument
  element.parserDocument = nil
  if parserDocument != nil and not element.attrb("async"):
    element.forceAsync = true
  let sourceText = element.childTextContent
  if not element.attrb("src") and sourceText == "":
    return
  if not element.isConnected:
    return
  let typeString = if element.attr("type") != "":
    element.attr("type").strip(chars = AsciiWhitespace).toLowerAscii()
  elif element.attr("language") != "":
    "text/" & element.attr("language").toLowerAscii()
  else:
    "text/javascript"
  if typeString.isJavaScriptType():
    element.ctype = CLASSIC
  elif typeString == "module":
    element.ctype = MODULE
  elif typeString == "importmap":
    element.ctype = IMPORTMAP
  else:
    return
  if parserDocument != nil:
    element.parserDocument = parserDocument
    element.forceAsync = false
  element.alreadyStarted = true
  element.preparationTimeDocument = element.document
  if parserDocument != nil and parserDocument != element.preparationTimeDocument:
    return
  if not element.scriptingEnabled:
    return
  if element.attrb("nomodule") and element.ctype == CLASSIC:
    return
  #TODO content security policy
  if element.ctype == CLASSIC and element.attrb("event") and element.attrb("for"):
    let f = element.attr("for").strip(chars = AsciiWhitespace)
    let event = element.attr("event").strip(chars = AsciiWhitespace)
    if not f.equalsIgnoreCase("window"):
      return
    if not event.equalsIgnoreCase("onload") and not event.equalsIgnoreCase("onload()"):
      return
  let cs = getCharset(element.attr("charset"))
  let encoding = if cs != CHARSET_UNKNOWN: cs else: element.document.charset
  let classicCORS = element.crossorigin
  var options = ScriptOptions(
    nonce: element.internalNonce,
    integrity: element.attr("integrity"),
    parserMetadata: if element.parserDocument != nil: PARSER_INSERTED else: NOT_PARSER_INSERTED,
    referrerpolicy: element.referrerpolicy
  )
  #TODO settings object
  if element.attrb("src"):
    if element.ctype == IMPORTMAP:
      #TODO fire error event
      return
    let src = element.attr("src")
    if src == "":
      #TODO fire error event
      return
    element.fromAnExternalFile = true
    let url = element.document.parseURL(src)
    if url.isNone:
      #TODO fire error event
      return
    if element.renderBlocking:
      element.blockRendering()
    element.delayingTheLoadEvent = true
    if element in element.document.renderBlockingElements:
      options.renderBlocking = true
    if element.ctype == CLASSIC:
      element.fetchClassicScript(url.get, options, classicCORS, encoding, markAsReady)
    else:
      #TODO MODULE
      element.markAsReady(ScriptResult(t: RESULT_NULL))
  else:
    let baseURL = element.document.baseURL
    if element.ctype == CLASSIC:
      let script = createClassicScript(sourceText, baseURL, options)
      element.markAsReady(ScriptResult(t: RESULT_SCRIPT, script: script))
    else:
      #TODO MODULE, IMPORTMAP
      element.markAsReady(ScriptResult(t: RESULT_NULL))
  if element.ctype == CLASSIC and element.attrb("src") or element.ctype == MODULE:
    let prepdoc = element.preparationTimeDocument 
    if element.attrb("async"):
      prepdoc.scriptsToExecSoon.add(element)
      element.onReady = (proc() =
        element.execute()
        let i = prepdoc.scriptsToExecSoon.find(element)
        element.preparationTimeDocument.scriptsToExecSoon.delete(i)
      )
    elif element.parserDocument == nil:
      prepdoc.scriptsToExecInOrder.addFirst(element)
      element.onReady = (proc() =
        if prepdoc.scriptsToExecInOrder.len > 0 and prepdoc.scriptsToExecInOrder[0] != element:
          while prepdoc.scriptsToExecInOrder.len > 0:
            let script = prepdoc.scriptsToExecInOrder[0]
            if script.scriptResult.t == RESULT_UNINITIALIZED:
              break
            script.execute()
            prepdoc.scriptsToExecInOrder.shrink(1)
      )
    elif element.ctype == MODULE or element.attrb("defer"):
      element.parserDocument.scriptsToExecOnLoad.addFirst(element)
      element.onReady = (proc() =
        element.readyForParserExec = true
      )
    else:
      element.parserDocument.parserBlockingScript = element
      element.blockRendering()
      element.onReady = (proc() =
        element.readyForParserExec = true
      )
  else:
    #TODO if CLASSIC, parserDocument != nil, parserDocument has a style sheet
    # that is blocking scripts, either the parser is an XML parser or a HTML
    # parser with a script level <= 1
    element.execute()

#TODO options/custom elements
proc createElement(document: Document, localName: string): Element {.jserr, jsfunc.} =
  if not localName.matchNameProduction():
    #TODO DOMException
    JS_ERR JS_TypeError, "InvalidCharacterError"
  let localName = if not document.isxml:
    localName.toLowerAscii2()
  else:
    localName
  let namespace = if not document.isxml: #TODO or content type is application/xhtml+xml
    Namespace.HTML
  else:
    NO_NAMESPACE
  return document.newHTMLElement(localName, namespace)

#TODO createElementNS

proc createDocumentFragment(document: Document): DocumentFragment {.jsfunc.} =
  return newDocumentFragment(document)

proc createDocumentType(implementation: DOMImplementation, qualifiedName, publicId, systemId: string): DocumentType {.jserr, jsfunc.} =
  if not qualifiedName.matchQNameProduction():
    #TODO should be DOMException
    JS_ERR JS_TypeError, "InvalidCharacterError"
  return implementation.document.newDocumentType(qualifiedName, publicId, systemId)

proc createHTMLDocument(implementation: DOMImplementation, title = none(string)): Document {.jsfunc.} =
  let doc = newDocument()
  doc.contentType = "text/html"
  doc.append(doc.newDocumentType("html"))
  let html = doc.newHTMLElement(TAG_HTML, Namespace.HTML)
  doc.append(html)
  let head = doc.newHTMLElement(TAG_HEAD, Namespace.HTML)
  html.append(head)
  if title.isSome:
    let titleElement = doc.newHTMLElement(TAG_TITLE, Namespace.HTML)
    titleElement.append(doc.newText(title.get))
    head.append(titleElement)
  html.append(doc.newHTMLElement(TAG_BODY, Namespace.HTML))
  #TODO set origin
  return doc

proc createCDATASection(document: Document, data: string): CDATASection {.jserr, jsfunc.} =
  if not document.isxml:
    #TODO should be DOMException
    JS_ERR JS_TypeError, "NotSupportedError"
  if "]]>" in data:
    #TODO should be DOMException
    JS_ERR JS_TypeError, "InvalidCharacterError"
  return newCDATASection(document, data)

proc createComment*(document: Document, data: string): Comment {.jsfunc.} =
  return newComment(document, data)

proc createProcessingInstruction(document: Document, target, data: string): ProcessingInstruction {.jsfunc.} =
  if not target.matchNameProduction() or "?>" in data:
    #TODO should be DOMException
    JS_ERR JS_TypeError, "InvalidCharacterError"
  return newProcessingInstruction(document, target, data)

# Forward definition hack (these are set in selectors.nim)
var doqsa*: proc (node: Node, q: string): seq[Element]
var doqs*: proc (node: Node, q: string): Element

proc querySelectorAll*(node: Node, q: string): seq[Element] {.jsfunc.} =
  return doqsa(node, q)

proc querySelector*(node: Node, q: string): Element {.jsfunc.} =
  return doqs(node, q)

const (ReflectTable, TagReflectMap, ReflectAllStartIndex) = (func(): (
    seq[ReflectEntry],
    Table[TagType, seq[uint16]],
    uint16) =
  var i: uint16 = 0
  while i < ReflectTable0.len:
    let x = ReflectTable0[i]
    result[0].add(x)
    if x.tags == AllTagTypes:
      break
    for tag in result[0][i].tags:
      if tag notin result[1]:
        result[1][tag] = newSeq[uint16]()
      result[1][tag].add(i)
    assert result[0][i].tags.len != 0
    inc i
  result[2] = i
  while i < ReflectTable0.len:
    let x = ReflectTable0[i]
    assert x.tags == AllTagTypes
    result[0].add(x)
    inc i
)()

proc jsReflectGet(ctx: JSContext, this: JSValue, magic: cint): JSValue {.cdecl.} =
  let entry = ReflectTable[uint16(magic)]
  let op = getOpaque0(this)
  if unlikely(not ctx.isInstanceOf(this, "Element") or op == nil):
    return JS_ThrowTypeError(ctx, "Reflected getter called on a value that is not an element")
  let element = cast[Element](op)
  if element.tagType notin entry.tags:
    return JS_ThrowTypeError(ctx, "Invalid tag type %s", element.tagType)
  case entry.t
  of REFLECT_STR:
    let x = toJS(ctx, element.attr(entry.attrname))
    return x
  of REFLECT_BOOl:
    return toJS(ctx, element.attrb(entry.attrname))
  of REFLECT_INT:
    return toJS(ctx, element.attri(entry.attrname).get(entry.i))
  of REFLECT_INT_GREATER_ZERO:
    return toJS(ctx, element.attrigz(entry.attrname).get(entry.i))
  of REFLECT_INT_GREATER_EQUAL_ZERO:
    return toJS(ctx, element.attrigez(entry.attrname).get(entry.i))

proc jsReflectSet(ctx: JSContext, this, val: JSValue, magic: cint): JSValue {.cdecl.} =
  if unlikely(not ctx.isInstanceOf(this, "Element")):
    return JS_ThrowTypeError(ctx, "Reflected getter called on a value that is not an element")
  let entry = ReflectTable[uint16(magic)]
  let op = getOpaque0(this)
  assert op != nil
  let element = cast[Element](op)
  if element.tagType notin entry.tags:
    return JS_ThrowTypeError(ctx, "Invalid tag type %s", element.tagType)
  case entry.t
  of REFLECT_STR:
    let x = toString(ctx, val)
    if x.isSome:
      element.attr(entry.attrname, x.get)
  of REFLECT_BOOL:
    let x = fromJS[bool](ctx, val)
    if x.isSome:
      if x.get:
        element.attr(entry.attrname, "")
      else:
        element.delAttr(entry.attrname)
  of REFLECT_INT:
    let x = fromJS[int](ctx, val)
    if x.isSome:
      element.attri(entry.attrname, x.get)
  of REFLECT_INT_GREATER_ZERO:
    let x = fromJS[int](ctx, val)
    if x.isSome:
      element.attrigz(entry.attrname, x.get)
  of REFLECT_INT_GREATER_EQUAL_ZERO:
    let x = fromJS[int](ctx, val)
    if x.isSome:
      element.attrigez(entry.attrname, x.get)
  return JS_DupValue(ctx, val)

proc addconsoleModule*(ctx: JSContext) =
  #TODO console should not have a prototype
  ctx.registerType(console, nointerface = true)

func getReflectFunctions(tags: set[TagType]): seq[TabGetSet] =
  for tag in tags:
    if tag in TagReflectMap:
      for i in TagReflectMap[tag]:
        result.add(TabGetSet(
          name: ReflectTable[i].funcname,
          get: jsReflectGet,
          set: jsReflectSet,
          magic: i
        ))
  return result

func getElementReflectFunctions(): seq[TabGetSet] =
  var i: uint16 = ReflectAllStartIndex
  while i < ReflectTable.len:
    let entry = ReflectTable[i]
    assert entry.tags == AllTagTypes
    result.add(TabGetSet(name: ReflectTable[i].funcname, get: jsReflectGet, set: jsReflectSet, magic: i))
    inc i

proc registerElements(ctx: JSContext, nodeCID: JSClassID) =
  let elementCID = ctx.registerType(Element, parent = nodeCID)
  const extra_getset = getElementReflectFunctions()
  let htmlElementCID = ctx.registerType(HTMLElement, parent = elementCID,
    extra_getset = extra_getset)
  template register(t: typed, tags: set[TagType]) =
    const extra_getset = getReflectFunctions(tags)
    ctx.registerType(t, parent = htmlElementCID,
      extra_getset = extra_getset)
  template register(t: typed, tag: TagType) =
    register(t, {tag})
  register(HTMLInputElement, TAG_INPUT)
  register(HTMLAnchorElement, TAG_A)
  register(HTMLSelectElement, TAG_SELECT)
  register(HTMLSpanElement, TAG_SPAN)
  register(HTMLOptGroupElement, TAG_OPTGROUP)
  register(HTMLOptionElement, TAG_OPTION)
  register(HTMLHeadingElement, {TAG_H1, TAG_H2, TAG_H3, TAG_H4, TAG_H5, TAG_H6})
  register(HTMLBRElement, TAG_BR)
  register(HTMLMenuElement, TAG_MENU)
  register(HTMLUListElement, TAG_UL)
  register(HTMLOListElement, TAG_OL)
  register(HTMLLIElement, TAG_LI)
  register(HTMLStyleElement, TAG_STYLE)
  register(HTMLLinkElement, TAG_LINK)
  register(HTMLFormElement, TAG_FORM)
  register(HTMLTemplateElement, TAG_TEMPLATE)
  register(HTMLUnknownElement, TAG_UNKNOWN)
  register(HTMLScriptElement, TAG_SCRIPT)
  register(HTMLBaseElement, TAG_BASE)
  register(HTMLAreaElement, TAG_AREA)
  register(HTMLButtonElement, TAG_BUTTON)
  register(HTMLTextAreaElement, TAG_TEXTAREA)
  register(HTMLLabelElement, TAG_LABEL)

proc addDOMModule*(ctx: JSContext) =
  let eventTargetCID = ctx.registerType(EventTarget)
  let nodeCID = ctx.registerType(Node, parent = eventTargetCID)
  ctx.registerType(NodeList)
  ctx.registerType(HTMLCollection)
  ctx.registerType(Document, parent = nodeCID)
  ctx.registerType(DOMImplementation)
  ctx.registerType(DOMTokenList)
  let characterDataCID = ctx.registerType(CharacterData, parent = nodeCID)
  ctx.registerType(Comment, parent = characterDataCID)
  ctx.registerType(CDATASection, parent = characterDataCID)
  ctx.registerType(DocumentFragment, parent = nodeCID)
  ctx.registerType(ProcessingInstruction, parent = characterDataCID)
  ctx.registerType(Text, parent = characterDataCID)
  ctx.registerType(DocumentType, parent = nodeCID)
  ctx.registerType(Attr, parent = nodeCID)
  ctx.registerType(NamedNodeMap)
  ctx.registerElements(nodeCID)