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)