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

              
               
                 
               
                


                    

                      
                       
                          

                 





















                                                           










                                                    



















                                                                        

                                                                           
 

















































                                                                        
                                                     







                                                                      










                                                                       


                                                                






















                                                                   

                                                                    








                                    
                     




                                           
                                 






                                           
                   
                               



                                                     
                          

   


































                                                                            








                                                                    



                                           
                    
 
                                                                        
                                   

                 









                                                          








                                                                             
import deques
import options
import streams

import html/dom
import html/enums
import js/error
import js/fromjs
import js/javascript
import types/url

import chakasu/charset

import chame/htmlparser
import chame/htmltokenizer
import chame/tags

# DOMBuilder implementation for Chawan.

type
  ChaDOMBuilder = ref object of DOMBuilder[Node]
    isFragment: bool

type DOMParser = ref object # JS interface

jsDestructor(DOMParser)

template getDocument(dombuilder: ChaDOMBuilder): Document =
  cast[Document](dombuilder.document)

proc finish(builder: DOMBuilder[Node]) =
  let builder = cast[ChaDOMBuilder](builder)
  let document = builder.getDocument()
  while document.scriptsToExecOnLoad.len > 0:
    #TODO spin event loop
    let script = document.scriptsToExecOnLoad.popFirst()
    script.execute()
  #TODO events

proc restart(builder: DOMBuilder[Node]) =
  let document = newDocument()
  document.contentType = "text/html"
  let oldDocument = cast[Document](builder.document)
  document.url = oldDocument.url
  let window = oldDocument.window
  if window != nil:
    document.window = window
    window.document = document
  builder.document = document

proc parseError(builder: DOMBuilder[Node], message: string) =
  discard

proc setQuirksMode(builder: DOMBuilder[Node], quirksMode: QuirksMode) =
  let builder = cast[ChaDOMBuilder](builder)
  let document = builder.getDocument()
  if not document.parser_cannot_change_the_mode_flag:
    document.mode = quirksMode

proc setCharacterSet(builder: DOMBuilder[Node], charset: Charset) =
  let builder = cast[ChaDOMBuilder](builder)
  let document = builder.getDocument()
  document.charset = charset

proc getTemplateContent(builder: DOMBuilder[Node], handle: Node): Node =
  return HTMLTemplateElement(handle).content

proc getTagType(builder: DOMBuilder[Node], handle: Node): TagType =
  return Element(handle).tagType

proc getParentNode(builder: DOMBuilder[Node], handle: Node): Option[Node] =
  return option(handle.parentNode)

proc getLocalName(builder: DOMBuilder[Node], handle: Node): string =
  return Element(handle).localName

proc getNamespace(builder: DOMBuilder[Node], handle: Node): Namespace =
  return Element(handle).namespace

proc createElement(builder: DOMBuilder[Node], localName: string,
    namespace: Namespace, tagType: TagType,
    attrs: Table[string, string]): Node =
  let builder = cast[ChaDOMBuilder](builder)
  let document = builder.getDocument()
  let element = document.newHTMLElement(localName, namespace,
    tagType = tagType, attrs = attrs)
  if element.isResettable():
    element.resetElement()
  if tagType == TAG_SCRIPT:
    let script = HTMLScriptElement(element)
    script.parserDocument = document
    script.forceAsync = false
    if builder.isFragment:
      script.alreadyStarted = true
      #TODO document.write (?)
  return element

proc createComment(builder: DOMBuilder[Node], text: string): Node =
  let builder = cast[ChaDOMBuilder](builder)
  return builder.getDocument().createComment(text)

proc createDocumentType(builder: DOMBuilder[Node], name, publicId,
    systemId: string): Node =
  let builder = cast[ChaDOMBuilder](builder)
  return builder.getDocument().newDocumentType(name, publicId, systemId)

proc insertBefore(builder: DOMBuilder[Node], parent, child,
    before: Node) =
  discard parent.insertBefore(child, before)

proc insertText(builder: DOMBuilder[Node], parent: Node, text: string,
    before: Node) =
  let builder = cast[ChaDOMBuilder](builder)
  let prevSibling = if before != nil:
    before.previousSibling
  else:
    parent.lastChild
  if prevSibling != nil and prevSibling.nodeType == TEXT_NODE:
    Text(prevSibling).data &= text
  else:
    let text = builder.getDocument().createTextNode(text)
    discard parent.insertBefore(text, before)

proc remove(builder: DOMBuilder[Node], child: Node) =
  child.remove(suppressObservers = true)

proc moveChildren(builder: DOMBuilder[Node], fromNode, toNode: Node) =
  var tomove = fromNode.childList
  for node in tomove:
    node.remove(suppressObservers = true)
  for child in tomove:
    toNode.insert(child, nil)

proc addAttrsIfMissing(builder: DOMBuilder[Node], element: Node,
    attrs: Table[string, string]) =
  let element = Element(element)
  for k, v in attrs:
    if not element.attrb(k):
      element.attr(k, v)

proc setScriptAlreadyStarted(builder: DOMBuilder[Node], script: Node) =
  HTMLScriptElement(script).alreadyStarted = true

proc associateWithForm(builder: DOMBuilder[Node], element, form,
    intendedParent: Node) =
  if form.inSameTree(intendedParent):
    #TODO remove following test eventually
    if Element(element).tagType in SupportedFormAssociatedElements:
      let element = FormAssociatedElement(element)
      element.setForm(HTMLFormElement(form))
      element.parserInserted = true

proc elementPopped(builder: DOMBuilder[Node], element: Node) =
  let builder = cast[ChaDOMBuilder](builder)
  let document = builder.getDocument()
  let element = Element(element)
  if element.tagType == TAG_TEXTAREA:
    element.resetElement()
  elif element.tagType == TAG_SCRIPT:
    #TODO microtask (maybe it works here too?)
    let script = HTMLScriptElement(element)
    #TODO document.write() (?)
    script.prepare()
    while document.parserBlockingScript != nil:
      let script = document.parserBlockingScript
      document.parserBlockingScript = nil
      #TODO style sheet
      script.execute()

proc newChaDOMBuilder(url: URL, window: Window, isFragment = false):
    ChaDOMBuilder =
  let document = newDocument()
  document.contentType = "text/html"
  document.url = url
  if window != nil:
    document.window = window
    window.document = document
  return ChaDOMBuilder(
    document: document,
    finish: finish,
    restart: restart,
    setQuirksMode: setQuirksMode,
    setCharacterSet: setCharacterset,
    elementPopped: elementPopped,
    getTemplateContent: getTemplateContent,
    getTagType: getTagType,
    getParentNode: getParentNode,
    getLocalName: getLocalName,
    getNamespace: getNamespace,
    createElement: createElement,
    createComment: createComment,
    createDocumentType: createDocumentType,
    insertBefore: insertBefore,
    insertText: insertText,
    remove: remove,
    moveChildren: moveChildren,
    addAttrsIfMissing: addAttrsIfMissing,
    setScriptAlreadyStarted: setScriptAlreadyStarted,
    associateWithForm: associateWithForm,
    #TODO isSVGIntegrationPoint (SVG support)
    isFragment: isFragment
  )

# https://html.spec.whatwg.org/multipage/parsing.html#parsing-html-fragments
proc parseHTMLFragment*(element: Element, s: string): seq[Node] =
  let url = parseURL("about:blank").get
  let builder = newChaDOMBuilder(url, nil)
  builder.isFragment = true
  let document = Document(builder.document)
  document.mode = element.document.mode
  let state = case element.tagType
  of TAG_TITLE, TAG_TEXTAREA: RCDATA
  of TAG_STYLE, TAG_XMP, TAG_IFRAME, TAG_NOEMBED, TAG_NOFRAMES: RAWTEXT
  of TAG_SCRIPT: SCRIPT_DATA
  of TAG_NOSCRIPT:
    if element.document != nil and element.document.scriptingEnabled:
      RAWTEXT
    else:
      DATA
  of TAG_PLAINTEXT:
    PLAINTEXT
  else: DATA
  let root = document.newHTMLElement(TAG_HTML)
  document.append(root)
  let opts = HTML5ParserOpts[Node](
    isIframeSrcdoc: false, #TODO?
    scripting: false,
    canReinterpret: false,
    charsets: @[CHARSET_UTF_8],
    ctx: some(Node(element)),
    initialTokenizerState: state,
    openElementsInit: @[Node(root)],
    pushInTemplate: element.tagType == TAG_TEMPLATE
  )
  let inputStream = newStringStream(s)
  parseHTML(inputStream, builder, opts)
  return root.childList

proc parseHTML*(inputStream: Stream, window: Window, url: URL,
    charsets: seq[Charset] = @[], canReinterpret = true): Document =
  let builder = newChaDOMBuilder(url, window)
  let opts = HTML5ParserOpts[Node](
    isIframeSrcdoc: false, #TODO?
    scripting: window != nil and window.settings.scripting,
    canReinterpret: canReinterpret,
    charsets: charsets
  )
  parseHTML(inputStream, builder, opts)
  return Document(builder.document)

proc newDOMParser(): DOMParser {.jsctor.} =
  return DOMParser()

proc parseFromString(ctx: JSContext, parser: DOMParser, str, t: string):
    JSResult[Document] {.jsfunc.} =
  case t
  of "text/html":
    let global = JS_GetGlobalObject(ctx)
    let window = if ctx.hasClass(Window):
      fromJS[Window](ctx, global).get(nil)
    else:
      Window(nil)
    JS_FreeValue(ctx, global)
    let url = if window != nil and window.document != nil:
      window.document.url
    else:
      newURL("about:blank").get
    let res = parseHTML(newStringStream(str), Window(nil), url)
    return ok(res)
  of "text/xml", "application/xml", "application/xhtml+xml", "image/svg+xml":
    return err(newInternalError("XML parsing is not supported yet"))
  else:
    return err(newTypeError("Invalid mime type"))

proc addHTMLModule*(ctx: JSContext) =
  ctx.registerType(DOMParser)