import std/deques import std/options import html/catom import html/dom import html/enums import js/error import js/fromjs import js/javascript import types/url import chagashi/charset import chame/htmlparser import chame/tags export htmlparser.ParseResult # DOMBuilder implementation for Chawan. type CharsetConfidence* = enum ccTentative, ccCertain, ccIrrelevant type HTML5ParserWrapper* = ref object parser: HTML5Parser[Node, CAtom] builder*: ChaDOMBuilder opts: HTML5ParserOpts[Node, CAtom] # hack so we don't have to worry about leaks or the GC deallocating parser refs: seq[Document] stoppedFromScript: bool ChaDOMBuilder = ref object of DOMBuilder[Node, CAtom] charset*: Charset confidence*: CharsetConfidence document*: Document factory: CAtomFactory poppedScript: HTMLScriptElement type DOMBuilderImpl = ChaDOMBuilder HandleImpl = Node AtomImpl = CAtom include chame/htmlparseriface type DOMParser = ref object # JS interface jsDestructor(DOMParser) #TODO this is disgusting and should be removed proc setActiveParser(document: Document, wrapper: HTML5ParserWrapper) = document.parser = cast[pointer](wrapper) wrapper.refs.add(document) proc getDocumentImpl(builder: ChaDOMBuilder): Node = return builder.document proc atomToTagTypeImpl(builder: ChaDOMBuilder, atom: CAtom): TagType = return builder.factory.toTagType(atom) proc tagTypeToAtomImpl(builder: ChaDOMBuilder, tagType: TagType): CAtom = return builder.factory.toAtom(tagType) proc strToAtomImpl(builder: ChaDOMBuilder, s: string): CAtom = return builder.factory.toAtom(s) proc finish(builder: ChaDOMBuilder) = while builder.document.scriptsToExecOnLoad.len > 0: #TODO spin event loop let script = builder.document.scriptsToExecOnLoad.popFirst() script.execute() #TODO events proc restart*(wrapper: HTML5ParserWrapper, charset: Charset) = let builder = wrapper.builder let document = newDocument(builder.factory) document.setActiveParser(wrapper) wrapper.refs.add(document) document.contentType = "text/html" let oldDocument = builder.document document.url = oldDocument.url let window = oldDocument.window if window != nil: document.window = window window.document = document builder.document = document builder.charset = charset assert document.factory != nil wrapper.parser = initHTML5Parser(builder, wrapper.opts) proc setQuirksModeImpl(builder: ChaDOMBuilder, quirksMode: QuirksMode) = if not builder.document.parser_cannot_change_the_mode_flag: builder.document.mode = quirksMode proc setEncodingImpl(builder: ChaDOMBuilder, encoding: string): SetEncodingResult = if builder.confidence != ccTentative: return SET_ENCODING_CONTINUE if builder.charset in {CHARSET_UTF_16_LE, CHARSET_UTF_16_BE}: builder.confidence = ccCertain return SET_ENCODING_CONTINUE let charset = getCharset(encoding) if charset == CHARSET_UNKNOWN: return SET_ENCODING_CONTINUE builder.confidence = ccCertain if charset == builder.charset: return SET_ENCODING_CONTINUE builder.charset = if charset == CHARSET_X_USER_DEFINED: CHARSET_WINDOWS_1252 else: charset return SET_ENCODING_STOP proc getTemplateContentImpl(builder: ChaDOMBuilder, handle: Node): Node = return HTMLTemplateElement(handle).content proc getParentNodeImpl(builder: ChaDOMBuilder, handle: Node): Option[Node] = return option(handle.parentNode) proc getLocalNameImpl(builder: ChaDOMBuilder, handle: Node): CAtom = return Element(handle).localName proc getNamespaceImpl(builder: ChaDOMBuilder, handle: Node): Namespace = return Element(handle).namespace proc createHTMLElementImpl(builder: ChaDOMBuilder): Node = return builder.document.newHTMLElement(TAG_HTML) proc createElementForTokenImpl(builder: ChaDOMBuilder, localName: CAtom, namespace: Namespace, intendedParent: Node, htmlAttrs: Table[CAtom, string], xmlAttrs: seq[ParsedAttr[CAtom]]): Node = let document = builder.document let element = document.newHTMLElement(localName, namespace) for k, v in htmlAttrs: element.attr(k, v) for attr in xmlAttrs: element.attrns(attr.name, attr.prefix, attr.namespace, attr.value) if element.tagType in ResettableElements: element.resetElement() if element of HTMLScriptElement: let script = HTMLScriptElement(element) script.parserDocument = document script.forceAsync = false # Note: per standard, we could set already started to true here when we # are parsing from document.write, but that sounds like a horrible idea. return element proc createCommentImpl(builder: ChaDOMBuilder, text: string): Node = return builder.document.createComment(text) proc createDocumentTypeImpl(builder: ChaDOMBuilder, name, publicId, systemId: string): Node = return builder.document.newDocumentType(name, publicId, systemId) proc insertBeforeImpl(builder: ChaDOMBuilder, parent, child: Node, before: Option[Node]) = discard parent.insertBefore(child, before.get(nil)) proc insertTextImpl(builder: ChaDOMBuilder, parent: Node, text: string, before: Option[Node]) = let prevSibling = if before.isSome: before.get.previousSibling else: parent.lastChild if prevSibling != nil and prevSibling of Text: Text(prevSibling).data &= text if parent of Element: Element(parent).invalid = true else: let text = builder.document.createTextNode(text) discard parent.insertBefore(text, before.get(nil)) proc removeImpl(builder: ChaDOMBuilder, child: Node) = child.remove(suppressObservers = true) proc moveChildrenImpl(builder: ChaDOMBuilder, fromNode, toNode: Node) = var tomove = fromNode.childList for node in tomove: node.remove(suppressObservers = true) for child in tomove: toNode.insert(child, nil) proc addAttrsIfMissingImpl(builder: ChaDOMBuilder, handle: Node, attrs: Table[CAtom, string]) = let element = Element(handle) for k, v in attrs: if not element.attrb(k): element.attr(k, v) proc setScriptAlreadyStartedImpl(builder: ChaDOMBuilder, script: Node) = HTMLScriptElement(script).alreadyStarted = true proc associateWithFormImpl(builder: ChaDOMBuilder, element, form, intendedParent: Node) = if form.inSameTree(intendedParent): #TODO remove following test eventually if element of FormAssociatedElement: let element = FormAssociatedElement(element) element.setForm(HTMLFormElement(form)) element.parserInserted = true proc elementPoppedImpl(builder: ChaDOMBuilder, element: Node) = let element = Element(element) if element of HTMLTextAreaElement: element.resetElement() elif element of HTMLScriptElement: assert builder.poppedScript == nil or not builder.document.scriptingEnabled builder.poppedScript = HTMLScriptElement(element) proc newChaDOMBuilder(url: URL, window: Window, factory: CAtomFactory, confidence: CharsetConfidence, charset = DefaultCharset): ChaDOMBuilder = let document = newDocument(factory) document.contentType = "text/html" document.url = url if window != nil: document.window = window window.document = document return ChaDOMBuilder( document: document, factory: factory, confidence: confidence, charset: charset ) # 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 factory = element.document.factory let builder = newChaDOMBuilder(url, nil, factory, ccIrrelevant) let 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, CAtom]( isIframeSrcdoc: false, #TODO? scripting: false, ctx: some((Node(element), element.localName)), initialTokenizerState: state, openElementsInit: @[(Node(root), root.localName)], pushInTemplate: element.tagType == TAG_TEMPLATE ) var parser = initHTML5Parser(builder, opts) let res = parser.parseChunk(s.toOpenArray(0, s.high)) # scripting is false and confidence is certain -> this must be continue assert res == PRES_CONTINUE parser.finish() builder.finish() return root.childList proc newHTML5ParserWrapper*(window: Window, url: URL, factory: CAtomFactory, confidence: CharsetConfidence, charset: Charset): HTML5ParserWrapper = let opts = HTML5ParserOpts[Node, CAtom](scripting: window.settings.scripting) let builder = newChaDOMBuilder(url, window, factory, confidence, charset) let wrapper = HTML5ParserWrapper( builder: builder, opts: opts, parser: initHTML5Parser(builder, opts) ) builder.document.setActiveParser(wrapper) return wrapper proc parseBuffer*(wrapper: HTML5ParserWrapper, buffer: openArray[char]): ParseResult = let builder = wrapper.builder let document = builder.document var res = wrapper.parser.parseChunk(buffer) # set insertion point for when it's needed var ip = wrapper.parser.getInsertionPoint() while res == PRES_SCRIPT: #TODO microtask let script = builder.poppedScript builder.poppedScript = nil document.writeBuffers.add(DocumentWriteBuffer()) script.prepare() while document.parserBlockingScript != nil: let script = document.parserBlockingScript document.parserBlockingScript = nil #TODO style sheet script.execute() assert document.parserBlockingScript != script if wrapper.stoppedFromScript: # document.write inserted a meta charset tag break assert document.writeBuffers[^1].toOA().len == 0 discard document.writeBuffers.pop() assert document.writeBuffers.len == 0 if ip == buffer.len: # script was at the end of the buffer; nothing to parse break # parse rest of input buffer res = wrapper.parser.parseChunk(buffer.toOpenArray(ip, buffer.high)) ip += wrapper.parser.getInsertionPoint() # move insertion point return res # Called from dom whenever document.write is executed. # We consume everything pushed into the top buffer. proc CDB_parseDocumentWriteChunk(wrapper: pointer) {.exportc.} = let wrapper = cast[HTML5ParserWrapper](wrapper) let builder = wrapper.builder let document = builder.document let buffer = document.writeBuffers[^1] var res = wrapper.parser.parseChunk(buffer.toOA()) if res == PRES_SCRIPT: document.writeBuffers.add(DocumentWriteBuffer()) while true: buffer.i += wrapper.parser.getInsertionPoint() #TODO microtask let script = builder.poppedScript builder.poppedScript = nil script.prepare() while document.parserBlockingScript != nil: let script = document.parserBlockingScript document.parserBlockingScript = nil #TODO style sheet script.execute() assert document.parserBlockingScript != script res = wrapper.parser.parseChunk(buffer.toOA()) if res != PRES_SCRIPT: break assert document.writeBuffers[^1].i == document.writeBuffers[^1].data.len discard document.writeBuffers.pop() assert builder.poppedScript == nil buffer.i = buffer.data.len if res == PRES_STOP: wrapper.stoppedFromScript = true proc finish*(wrapper: HTML5ParserWrapper) = wrapper.parser.finish() wrapper.builder.finish() for r in wrapper.refs: r.parser = nil wrapper.refs.setLen(0) 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 #TODO this is probably broken in client (or at least sub-optimal) let factory = if window != nil: window.factory else: newCAtomFactory() let builder = newChaDOMBuilder(url, window, factory, ccIrrelevant) var parser = initHTML5Parser(builder, HTML5ParserOpts[Node, CAtom]()) let res = parser.parseChunk(str) assert res == PRES_CONTINUE parser.finish() builder.finish() return ok(builder.document) 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)