diff options
author | bptato <nincsnevem662@gmail.com> | 2022-12-18 20:46:30 +0100 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2022-12-18 20:46:30 +0100 |
commit | bfaf210d87e90016f8f2521657bd04686170aa43 (patch) | |
tree | e9711cb2f72174058d88ce2d52a76239e3c54c62 /src/html | |
parent | 1fbe17eeddefb87bf8e819be7792ae7a6482d8f8 (diff) | |
download | chawan-bfaf210d87e90016f8f2521657bd04686170aa43.tar.gz |
Add JS support to documents
Diffstat (limited to 'src/html')
-rw-r--r-- | src/html/dom.nim | 544 | ||||
-rw-r--r-- | src/html/env.nim | 26 | ||||
-rw-r--r-- | src/html/htmlparser.nim | 68 |
3 files changed, 571 insertions, 67 deletions
diff --git a/src/html/dom.nim b/src/html/dom.nim index 153b91a1..c80c7201 100644 --- a/src/html/dom.nim +++ b/src/html/dom.nim @@ -1,12 +1,20 @@ +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 @@ -31,7 +39,62 @@ type 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 + settings*: EnvironmentSettings + loader*: Option[FileLoader] + jsrt*: JSRuntime + jsctx*: JSContext + document* {.jsget.}: Document + console* {.jsget.}: Console + + Console* = ref object + err: Stream + + NamedNodeMap* = ref object + element: Element + attrlist: seq[string] + attrs: Table[string, Attr] + nsattrs: Table[Namespace, Table[string, Attr]] + + EnvironmentSettings* = object + scripting*: bool + EventTarget* = ref object of RootObj Node* = ref object of EventTarget @@ -45,20 +108,32 @@ type document*: Document Attr* = ref object of Node - namespaceURI*: string - prefix*: string - localName*: string - name*: string - value*: string - ownerElement*: Element + namespaceURI* {.jsget.}: string + prefix* {.jsget.}: string + localName* {.jsget.}: string + value* {.jsget.}: string + ownerElement* {.jsget.}: Element Document* = ref object of Node - location*: Url + charset*: Charset + window*: Window + url*: URL #TODO expose as URL (capitalized) + location {.jsget.}: URL #TODO should be location mode*: QuirksMode + currentScript: HTMLScriptElement + isxml*: bool + + 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*: string + + renderBlockingElements: seq[Element] CharacterData* = ref object of Node data*: string @@ -78,14 +153,17 @@ type Element* = ref object of Node namespace*: Namespace - namespacePrefix*: Option[string] #TODO namespaces + namespacePrefix*: Option[string] prefix*: string localName*: string tagType*: TagType id*: string classList*: seq[string] - attributes* {.jsget, jsset.}: Table[string, string] + # attrs and nsattrs both store qualified names. + attrs*: Table[string, string] # namespace = null + nsattrs: Table[Namespace, Table[string, string]] + attrsmap: NamedNodeMap hover*: bool invalid*: bool @@ -160,11 +238,13 @@ type preparationTimeDocument*: Document forceAsync*: bool fromAnExternalFile*: bool - readyToBeParser*: bool + readyForParserExec*: bool alreadyStarted*: bool - delayingTheLoadEvent*: bool - ctype*: bool - #TODO result + delayingTheLoadEvent: bool + ctype: ScriptType + internalNonce: string + scriptResult*: ScriptResult + onReady: (proc()) HTMLBaseElement* = ref object of HTMLElement @@ -214,7 +294,7 @@ func `$`*(node: Node): string = of ELEMENT_NODE: let element = Element(node) result = "<" & $element.tagType.tostr() - for k, v in element.attributes: + for k, v in element.attrs: result &= ' ' & k & "=\"" & v.escapeText(true) & "\"" result &= ">\n" for node in element.childNodes: @@ -321,6 +401,163 @@ iterator options*(select: HTMLSelectElement): HTMLOptionElement {.inline.} = if opt.tagType == TAG_OPTION: yield HTMLOptionElement(child) +func newAttr(parent: Element, qualifiedName, value: string): Attr = + new(result) + result.document = parent.document + result.nodeType = ATTRIBUTE_NODE + result.ownerElement = parent + if parent.namespace == Namespace.HTML: + result.localName = qualifiedName + else: + #TODO ??? + let s = qualifiedName.until(':') + if s.len == qualifiedName.len: + result.localName = qualifiedName + else: + result.prefix = s + result.localName = qualifiedName.substr(s.len) + result.value = value + +func name(attr: Attr): string {.jsfget.} = + if attr.prefix == "": + return attr.localName + return attr.prefix & attr.localName + +#TODO TODO TODO all of this is dumb, we should just have an array of attrs, that's all +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 + for ns, t in element.nsattrs: + if qualifiedName in t: + return true + +func hasAttributeNS(element: Element, namespace, localName: string): bool {.jsfunc.} = + if namespace == "": + return localName in element.attrs + for ns, t in element.nsattrs: + if $ns == namespace and localName in t: + return true + +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[]) + for ns, t in element.nsattrs.mpairs: + t.withValue(qualifiedName, val): + return some(val[]) + +func getAttributeNS(element: Element, namespace, localName: string): Option[string] {.jsfunc.} = + if namespace == "": + return element.getAttribute(localName) + if namespace == $Namespace.HTML: + return element.getAttribute(localName) + try: + #TODO TODO TODO parseEnum is style insensitive + let ns = parseEnum[Namespace](namespace) + element.nsattrs.withValue(ns, t): + t[].withValue(localName, val): + # Not a qualified name... + return some(val[]) + for k, v in t[]: + let s = k.until(':') + if s.len != k.len and localName == s: + return some(v) + except ValueError: + discard + +#TODO this is simply wrong +func getNamedItem(map: NamedNodeMap, qualifiedName: string): Option[Attr] {.jsfunc.} = + let v = map.element.getAttribute(qualifiedName) + if v.isSome: + if qualifiedName notin map.attrs: + map.attrs[qualifiedName] = map.element.newAttr(qualifiedName, v.get) + return some(map.attrs[qualifiedName]) + +func getNamedItemNS(map: NamedNodeMap, namespace, localName: string): Option[Attr] {.jsfunc.} = + if namespace == "": + return map.getNamedItem(localName) + if namespace == $Namespace.HTML: + return map.getNamedItem(localName) + try: + #TODO TODO TODO parseEnum is style insensitive + let ns = parseEnum[Namespace](namespace) + var qn: string + if ns notin map.nsattrs: + map.nsattrs[ns] = Table[string, Attr]() + map.element.nsattrs.withValue(ns, t): + t[].withValue(localName, val): + # Not a qualified name... + if localName notin map.nsattrs[ns]: + map.nsattrs[ns][localName] = map.element.newAttr(localName, val[]) + qn = localName + for k, v in t[]: + let s = k.until(':') + if localName == s: + if localName notin map.nsattrs[ns]: + map.nsattrs[ns][localName] = map.element.newAttr(localName, v) + qn = k + if qn != "": + return some(map.nsattrs[ns][qn]) + except ValueError: + discard + +func attributes(element: Element): NamedNodeMap {.jsfget.} = + if element.attrsmap == nil: + element.attrsmap = NamedNodeMap(element: element) + return element.attrsmap + +func length(map: NamedNodeMap): int {.jsfget.} = + return map.element.attrs.len + +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 oldAttr = getNamedItem(map, attr.name) + map.element.attrs[attr.name] = attr.value + return oldAttr + +proc setNamedItemNS*(map: NamedNodeMap, attr: Attr): Option[Attr] {.jsfunc.} = + map.setNamedItem(attr) + +#TODO TODO TODO this is extremely inefficient... +func item(map: NamedNodeMap, i: int): Option[Attr] {.jsfunc.} = + var found: HashSet[string] + for j in countdown(map.attrlist.high, 0): + let k = map.attrlist[j] + if k in map.element.attrs: + found.incl(k) + else: + map.attrlist.delete(j) + if map.attrlist.len < map.element.attrs.len: + for k in map.element.attrs.keys: + if k notin found: + map.attrlist.add(k) + if i < map.attrlist.len: + return map.getNamedItem(map.attrlist[i]) + +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 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 @@ -484,7 +721,7 @@ func nextElementSibling*(elem: Element): Element = return nil func attr*(element: Element, s: string): string {.inline.} = - return element.attributes.getOrDefault(s, "") + return element.attrs.getOrDefault(s, "") func attri*(element: Element, s: string): Option[int] = let a = element.attr(s) @@ -494,7 +731,7 @@ func attri*(element: Element, s: string): Option[int] = return none(int) func attrb*(element: Element, s: string): bool = - if s in element.attributes: + if s in element.attrs: return true return false @@ -521,6 +758,19 @@ func childTextContent*(node: Node): string = if child.nodeType == TEXT_NODE: result &= Text(child).data +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*(element: Element): seq[CSSStylesheet] = for child in element.children: if child.tagType == TAG_STYLE: @@ -655,6 +905,10 @@ func newText*(document: Document, data: string = ""): Text {.jsctor.} = result.document = document result.data = data +func textContent*(node: Node, data: string) {.jsfset.} = + node.childNodes.setLen(0) + node.childNodes.add(node.document.newText(data)) + func newComment*(document: Document = nil, data: string = ""): Comment {.jsctor.} = new(result) result.nodeType = COMMENT_NODE @@ -729,6 +983,7 @@ func newDocument*(): Document {.jsctor.} = new(result) result.nodeType = DOCUMENT_NODE result.document = result + result.contentType = "text/html" func newDocumentType*(document: Document, name: string, publicId = "", systemId = ""): DocumentType {.jsctor.} = new(result) @@ -737,14 +992,6 @@ func newDocumentType*(document: Document, name: string, publicId = "", systemId result.publicId = publicId result.systemId = systemId -func newAttr*(parent: Element, key, value: string): Attr = - new(result) - result.document = parent.document - result.nodeType = ATTRIBUTE_NODE - result.ownerElement = parent - result.name = key - result.value = value - func getElementById*(node: Node, id: string): Element {.jsfunc.} = if id.len == 0: return nil @@ -783,21 +1030,28 @@ func isHostIncludingInclusiveAncestor*(a, b: Node): bool = return true return false -func baseUrl*(document: Document): Url = +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.location - let url = parseUrl(href, document.location.some) - if url.isnone: - return document.location + 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 href*[T: HTMLAnchorElement|HTMLLinkElement|HTMLBaseElement](element: T): string = if element.attrb("href"): - let url = parseUrl(element.attr("href"), some(element.document.location)) + let url = parseUrl(element.attr("href"), some(element.document.url)) if url.issome: return $url.get return "" @@ -1051,15 +1305,15 @@ proc reset*(form: HTMLFormElement) = proc appendAttributes*(element: Element, attrs: Table[string, string]) = for k, v in attrs: - element.attributes[k] = v + element.attrs[k] = v template reflect_str(element: Element, name: static string, val: untyped) = - element.attributes.withValue(name, val): + element.attrs.withValue(name, val): element.val = val[] template reflect_str(element: Element, name: static string, val, fun: untyped) = - element.attributes.withValue(name, val): + element.attrs.withValue(name, val): element.val = fun(val[]) template reflect_nonzero_int(element: Element, name: static string, val: untyped, default: int) = - element.attributes.withValue(name, val): + element.attrs.withValue(name, val): if val[].isValidNonZeroInt(): element.val = parseInt(val[]) else: @@ -1067,14 +1321,13 @@ proc appendAttributes*(element: Element, attrs: Table[string, string]) = do: element.val = default template reflect_bool(element: Element, name: static string, val: untyped) = - if name in element.attributes: + if name in element.attrs: element.val = true element.reflect_str "id", id - element.attributes.withValue("class", val): - let classList = val[].split(' ') - for x in classList: - if x != "" and x notin element.classList: - element.classList.add(x) + let classList = element.attr("class").split(' ') + for x in classList: + if x != "" and x notin element.classList: + element.classList.add(x) case element.tagType of TAG_INPUT: let input = HTMLInputElement(element) @@ -1087,7 +1340,7 @@ proc appendAttributes*(element: Element, attrs: Table[string, string]) = option.reflect_bool "selected", selected of TAG_SELECT: let select = HTMLSelectElement(element) - select.reflect_nonzero_int "size", size, (if "multiple" in element.attributes: 4 else: 1) + select.reflect_nonzero_int "size", size, (if "multiple" in element.attrs: 4 else: 1) of TAG_BUTTON: let button = HTMLButtonElement(element) button.reflect_str "type", ctype, (func(s: string): ButtonType = @@ -1099,7 +1352,214 @@ proc appendAttributes*(element: Element, attrs: Table[string, string]) = let textarea = HTMLTextAreaElement(element) textarea.reflect_nonzero_int "cols", cols, 20 textarea.reflect_nonzero_int "rows", rows, 1 + of TAG_SCRIPT: + let element = HTMLScriptElement(element) + element.reflect_str "nonce", internalNonce + else: discard + +proc renderBlocking*(element: Element): bool = + if "render" in element.attr("blocking").split(AsciiWhitespace): + return true + case element.tagType + of 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 else: discard + 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 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 ret = document.window.jsctx.eval(element.scriptResult.script.record, "<script>", JS_EVAL_TYPE_GLOBAL) + if JS_IsException(ret): + let ss = newStringStream() + document.window.jsctx.writeException(ss) + ss.setPosition(0) + eprint "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.connected: + 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() # Forward definition hack (these are set in selectors.nim) var doqsa*: proc (node: Node, q: string): seq[Element] @@ -1120,6 +1580,8 @@ proc addDOMModule*(ctx: JSContext) = ctx.registerType(Text, parent = characterDataCID) ctx.registerType(DocumentType, parent = nodeCID) let elementCID = ctx.registerType(Element, parent = nodeCID) + ctx.registerType(Attr) + ctx.registerType(NamedNodeMap) let htmlElementCID = ctx.registerType(HTMLElement, parent = elementCID) ctx.registerType(HTMLInputElement, parent = htmlElementCID) ctx.registerType(HTMLAnchorElement, parent = htmlElementCID) diff --git a/src/html/env.nim b/src/html/env.nim new file mode 100644 index 00000000..6db018c9 --- /dev/null +++ b/src/html/env.nim @@ -0,0 +1,26 @@ +import html/dom +import html/htmlparser +import io/loader +import js/javascript +import types/url + +proc newWindow*(scripting: bool, loader = none(FileLoader)): Window = + result = Window( + loader: loader, + settings: EnvironmentSettings( + scripting: scripting + ) + ) + if scripting: + let rt = newJSRuntime() + let ctx = rt.newJSContext() + result.jsrt = rt + result.jsctx = ctx + var global = JS_GetGlobalObject(ctx) + ctx.registerType(Window, asglobal = true) + ctx.setOpaque(global, result) + ctx.setProperty(global, "window", global) + JS_FreeValue(ctx, global) + ctx.addDOMModule() + ctx.addURLModule() + ctx.addHTMLModule() diff --git a/src/html/htmlparser.nim b/src/html/htmlparser.nim index f04a94f7..999625a8 100644 --- a/src/html/htmlparser.nim +++ b/src/html/htmlparser.nim @@ -1,3 +1,4 @@ +import deques import macros import options import sequtils @@ -9,11 +10,12 @@ import unicode import css/sheet import data/charset +import encoding/decoderstream import html/dom import html/tags import html/htmltokenizer -import encoding/decoderstream import js/javascript +import types/url import utils/twtstr type @@ -205,8 +207,6 @@ func createElement(parser: HTML5Parser, token: Token, namespace: Namespace, inte let localName = token.tagname let element = document.newHTMLElement(localName, namespace, tagType = token.tagtype) element.appendAttributes(token.attrs) - #for k, v in token.attrs: - # element.appendAttribute(k, v) if element.isResettable(): element.resetElement() @@ -232,6 +232,8 @@ proc popElement(parser: var HTML5Parser): Element = else: parser.tokenizer.hasnonhtml = not parser.adjustedCurrentNode().inHTMLNamespace() +template pop_current_node = discard parser.popElement() + proc insert(location: AdjustedInsertionLocation, node: Node) = location.inside.insert(node, location.before) @@ -504,16 +506,16 @@ proc pushOntoActiveFormatting(parser: var HTML5Parser, element: Element, token: if it[0].localName != element.localName: continue if it[0].namespace != element.namespace: continue var fail = false - for k, v in it[0].attributes: - if k notin element.attributes: + for k, v in it[0].attrs: + if k notin element.attrs: fail = true break - if v != element.attributes[k]: + if v != element.attrs[k]: fail = true break if fail: continue - for k, v in element.attributes: - if k notin it[0].attributes: + for k, v in element.attrs: + if k notin it[0].attrs: fail = true break if fail: continue @@ -557,8 +559,6 @@ proc reconstructActiveFormatting(parser: var HTML5Parser) = proc clearActiveFormattingTillMarker(parser: var HTML5Parser) = while parser.activeFormatting.len > 0 and parser.activeFormatting.pop()[0] != nil: discard -template pop_current_node = discard parser.popElement() - func isHTMLIntegrationPoint(node: Element): bool = return false #TODO SVG (NOTE MathML not implemented) @@ -1138,8 +1138,8 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = discard else: for k, v in token.attrs: - if k notin parser.openElements[0].attributes: - parser.openElements[0].attributes[k] = v + if k notin parser.openElements[0].attrs: + parser.openElements[0].attrs[k] = v ) ("<base>", "<basefont>", "<bgsound>", "<link>", "<meta>", "<noframes>", "<script>", "<style>", "<template>", "<title>", "</template>") => (block: parser.processInHTMLContent(token, IN_HEAD)) @@ -1150,8 +1150,8 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = else: parser.framesetOk = false for k, v in token.attrs: - if k notin parser.openElements[1].attributes: - parser.openElements[1].attributes[k] = v + if k notin parser.openElements[1].attrs: + parser.openElements[1].attrs[k] = v ) "<frameset>" => (block: parse_error @@ -1523,11 +1523,15 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = ) "</script>" => (block: #TODO microtask - pop_current_node + let script = HTMLScriptElement(parser.popElement()) parser.insertionMode = parser.oldInsertionMode - #TODO document.write() ? - #TODO prepare script element - #TODO uh implement scripting or something + #TODO document.write() (?) + script.prepare() + while parser.document.parserBlockingScript != nil: + let script = parser.document.parserBlockingScript + parser.document.parserBlockingScript = nil + #TODO style sheet + script.execute() ) TokenType.END_TAG => (block: pop_current_node @@ -2164,12 +2168,18 @@ proc constructTree(parser: var HTML5Parser): Document = if parser.needsreinterpret: return nil - #TODO document.write (?) - #TODO etc etc... - return parser.document -proc parseHTML5*(inputStream: Stream, cs = none(Charset), fallbackcs = CHARSET_UTF_8): (Document, Charset) = +proc finishParsing(parser: var HTML5Parser) = + while parser.openElements.len > 0: + pop_current_node + while parser.document.scriptsToExecOnLoad.len > 0: + #TODO spin event loop + let script = parser.document.scriptsToExecOnLoad.popFirst() + script.execute() + #TODO events + +proc parseHTML*(inputStream: Stream, cs = none(Charset), fallbackcs = CHARSET_UTF_8, window: Window = nil, url: URL = nil): (Document, Charset) = var parser: HTML5Parser var bom: string if cs.isSome: @@ -2201,8 +2211,14 @@ proc parseHTML5*(inputStream: Stream, cs = none(Charset), fallbackcs = CHARSET_U for c in bom: decoder.prepend(cast[uint32](c)) parser.document = newDocument() + if window != nil: + parser.document.window = window + window.document = parser.document + parser.document.url = url parser.tokenizer = newTokenizer(decoder) - return (parser.constructTree(), parser.charset) + let document = parser.constructTree() + parser.finishParsing() + return (document, parser.charset) proc newDOMParser*(): DOMParser {.jsctor.} = new(result) @@ -2210,12 +2226,12 @@ proc newDOMParser*(): DOMParser {.jsctor.} = proc parseFromString*(parser: DOMParser, str: string, t: string): Document {.jserr, jsfunc.} = case t of "text/html": - let (res, _) = parseHTML5(newStringStream(str)) + let (res, _) = parseHTML(newStringStream(str)) return res of "text/xml", "application/xml", "application/xhtml+xml", "image/svg+xml": - JS_THROW JS_InternalError, "XML parsing is not supported yet" + JS_ERR JS_InternalError, "XML parsing is not supported yet" else: - JS_THROW JS_TypeError, "Invalid mime type" + JS_ERR JS_TypeError, "Invalid mime type" proc addHTMLModule*(ctx: JSContext) = ctx.registerType(DOMParser) |