diff options
-rw-r--r-- | src/html/catom.nim | 42 | ||||
-rw-r--r-- | src/html/chadombuilder.nim | 4 | ||||
-rw-r--r-- | src/html/dom.nim | 149 | ||||
-rw-r--r-- | src/html/env.nim | 10 | ||||
-rw-r--r-- | src/html/event.nim | 46 | ||||
-rw-r--r-- | src/html/xmlhttprequest.nim | 305 | ||||
-rw-r--r-- | src/loader/headers.nim | 49 | ||||
-rw-r--r-- | src/loader/response.nim | 25 | ||||
-rw-r--r-- | src/server/buffer.nim | 128 | ||||
-rw-r--r-- | src/utils/twtstr.nim | 27 | ||||
-rw-r--r-- | test/js/click_setter.html | 13 | ||||
-rw-r--r-- | test/js/xhr.html | 17 |
12 files changed, 617 insertions, 198 deletions
diff --git a/src/html/catom.nim b/src/html/catom.nim index 7c61e23e..b6dced60 100644 --- a/src/html/catom.nim +++ b/src/html/catom.nim @@ -4,6 +4,11 @@ import std/sets import std/strutils import chame/tags +import monoucha/fromjs +import monoucha/javascript +import monoucha/jserror +import monoucha/tojs +import types/opt # create a static enum compatible with chame/tags @@ -11,6 +16,7 @@ macro makeStaticAtom = # declare inside the macro to avoid confusion with StaticAtom0 type StaticAtom0 = enum + satAbort = "abort" satAcceptCharset = "accept-charset" satAction = "action" satAlign = "align" @@ -23,14 +29,17 @@ macro makeStaticAtom = satChecked = "checked" satClass = "class" satClassList = "classList" + satClick = "click" satColor = "color" satCols = "cols" satColspan = "colspan" satCrossorigin = "crossorigin" + satDOMContentLoaded = "DOMContentLoaded" satDefer = "defer" satDirname = "dirname" satDisabled = "disabled" satEnctype = "enctype" + satError = "error" satEvent = "event" satFor = "for" satForm = "form" @@ -43,16 +52,22 @@ macro makeStaticAtom = satIntegrity = "integrity" satIsmap = "ismap" satLanguage = "language" - satMax = "max", + satLoad = "load" + satLoadend = "loadend" + satLoadstart = "loadstart" + satMax = "max" satMedia = "media" satMethod = "method" - satMin = "min", + satMin = "min" + satMousewheel = "mousewheel" satMultiple = "multiple" satName = "name" satNomodule = "nomodule" satNovalidate = "novalidate" satOnclick = "onclick" satOnload = "onload" + satProgress = "progress" + satReadystatechange = "readystatechange" satReferrerpolicy = "referrerpolicy" satRel = "rel" satRequired = "required" @@ -68,11 +83,15 @@ macro makeStaticAtom = satStylesheet = "stylesheet" satTarget = "target" satText = "text" + satTimeout = "timeout" satTitle = "title" + satTouchmove = "touchmove" + satTouchstart = "touchstart" satType = "type" satUsemap = "usemap" satValign = "valign" satValue = "value" + satWheel = "wheel" satWidth = "width" let decl = quote do: type StaticAtom* {.inject.} = enum @@ -177,3 +196,22 @@ func toStaticAtom*(factory: CAtomFactory; atom: CAtom): StaticAtom = if i <= int(StaticAtom.high): return StaticAtom(i) return atUnknown + +var getFactory* {.compileTime.}: proc(ctx: JSContext): CAtomFactory {.nimcall, + noSideEffect.} + +proc toAtom*(ctx: JSContext; atom: StaticAtom): CAtom = + return ctx.getFactory().toAtom(atom) + +proc toStaticAtom*(ctx: JSContext; atom: CAtom): StaticAtom = + return ctx.getFactory().toStaticAtom(atom) + +proc fromJSCAtom*(ctx: JSContext; val: JSValue): JSResult[CAtom] = + let s = ?fromJS[string](ctx, val) + return ok(ctx.getFactory().toAtom(s)) + +proc toJS*(ctx: JSContext; atom: CAtom): JSValue = + return ctx.toJS(ctx.getFactory().toStr(atom)) + +proc toJS*(ctx: JSContext; atom: StaticAtom): JSValue = + return ctx.toJS($atom) diff --git a/src/html/chadombuilder.nim b/src/html/chadombuilder.nim index 2bb9bec7..5470deb2 100644 --- a/src/html/chadombuilder.nim +++ b/src/html/chadombuilder.nim @@ -356,9 +356,7 @@ proc parseFromString(ctx: JSContext; parser: DOMParser; str, t: string): JSResult[Document] {.jsfunc.} = case t of "text/html": - let global = JS_GetGlobalObject(ctx) - let window = fromJS[Window](ctx, global).get - JS_FreeValue(ctx, global) + let window = ctx.getWindow() let url = if window.document != nil: window.document.url else: diff --git a/src/html/dom.nim b/src/html/dom.nim index 9f8c1f2c..9b092e3d 100644 --- a/src/html/dom.nim +++ b/src/html/dom.nim @@ -35,6 +35,7 @@ import monoucha/jserror import monoucha/jsopaque import monoucha/jspropenumlist import monoucha/jsutils +import monoucha/quickjs import monoucha/tojs import types/blob import types/color @@ -767,11 +768,11 @@ type of rtUlong, rtUlongGz: u: uint32 of rtFunction: - ctype: string + ctype: StaticAtom else: discard -func attrType0(s: static string): StaticAtom = - return parseEnum[StaticAtom](s) +func attrType0(s: static string): StaticAtom {.compileTime.} = + return strictParseEnum[StaticAtom](s).get template toset(ts: openArray[TagType]): set[TagType] = var tags: system.set[TagType] @@ -840,14 +841,14 @@ func makeulgz(name: static string; ts: varargs[TagType]; default = 0u32): u: default ) -func makef(name: static string; ts: set[TagType]; ctype: string): ReflectEntry = +func makef(name: static string; ts: set[TagType]; ctype: static string): ReflectEntry = const attrname = attrType0(name) ReflectEntry( attrname: attrname, funcname: name, t: rtFunction, tags: ts, - ctype: ctype + ctype: attrType0(ctype) ) const ReflectTable0 = [ @@ -891,11 +892,35 @@ func baseURL*(document: Document): URL proc delAttr(element: Element; i: int; keep = false) proc reflectAttr(element: Element; name: CAtom; value: Option[string]) +# For now, these are the same; on an API level however, getGlobal is guaranteed +# to be non-null, while getWindow may return null in the future. (This is in +# preparation for Worker support.) +func getGlobal*(ctx: JSContext): Window = + let global = JS_GetGlobalObject(ctx) + let window = fromJS[Window](ctx, global).get + JS_FreeValue(ctx, global) + return window + +func getWindow*(ctx: JSContext): Window = + let global = JS_GetGlobalObject(ctx) + let window = fromJS[Window](ctx, global).get + JS_FreeValue(ctx, global) + return window + func document*(node: Node): Document = if node of Document: return Document(node) return node.internalDocument +proc toAtom*(window: Window; atom: StaticAtom): CAtom = + return window.factory.toAtom(atom) + +proc toAtom*(window: Window; s: string): CAtom = + return window.factory.toAtom(s) + +proc toStr*(window: Window; atom: CAtom): string = + return window.factory.toStr(atom) + proc toAtom*(document: Document; s: string): CAtom = return document.factory.toAtom(s) @@ -1029,6 +1054,12 @@ iterator ancestors*(node: Node): Element {.inline.} = yield element element = element.parentElement +iterator nodeAncestors*(node: Node): Node {.inline.} = + var node = node.parentNode + while node != nil: + yield node + node = node.parentNode + # Returns the node itself and its ancestors iterator branch*(node: Node): Node {.inline.} = var node = node @@ -2160,7 +2191,7 @@ func serializesAsVoid(element: Element): bool = const Extra = {TAG_BASEFONT, TAG_BGSOUND, TAG_FRAME, TAG_KEYGEN, TAG_PARAM} return element.tagType in VoidElements + Extra -func serializeFragment(node: Node): string +func serializeFragment*(node: Node): string func serializeFragmentInner(child: Node; parentType: TagType): string = result = "" @@ -2198,7 +2229,7 @@ func serializeFragmentInner(child: Node; parentType: TagType): string = elif child of DocumentType: result &= "<!DOCTYPE " & DocumentType(child).name & '>' -func serializeFragment(node: Node): string = +func serializeFragment*(node: Node): string = var node = node var parentType = TAG_UNKNOWN if node of Element: @@ -2416,6 +2447,54 @@ isDefaultPassive = func(eventTarget: EventTarget): bool = EventTarget(node.document.html) == eventTarget or EventTarget(node.document.body) == eventTarget +getFactory = proc(ctx: JSContext): CAtomFactory = + return ctx.getGlobal().factory + +type DispatchEventResult = tuple + called: bool + canceled: bool + +proc dispatchEvent0(window: Window; event: Event; currentTarget: EventTarget; + called, stop, canceled: var bool) = + let ctx = window.jsctx + event.currentTarget = currentTarget + var els = currentTarget.eventListeners # copy intentionally + for el in els: + if el.ctype == event.ctype: + let e = ctx.invoke(el, event) + called = true + if JS_IsException(e): + window.console.error(ctx.getExceptionMsg()) + JS_FreeValue(ctx, e) + if efStopImmediatePropagation in event.flags: + stop = true + break + if efStopPropagation in event.flags: + stop = true + if efCanceled in event.flags: + canceled = true + +proc dispatchEvent*(window: Window; event: Event; target: EventTarget): + DispatchEventResult = + #TODO this is far from being compliant + var called = false + var canceled = false + let ctx = window.jsctx + var jsEvent = ctx.toJS(event) + var stop = false + window.dispatchEvent0(event, target, called, stop, canceled) + if not stop and target of Node: + for a in Node(target).nodeAncestors: + window.dispatchEvent0(event, a, called, stop, canceled) + if stop: + break + JS_FreeValue(ctx, jsEvent) + return (called, canceled) + +proc fireEvent*(window: Window; name: StaticAtom; target: EventTarget) = + let event = newEvent(window.toAtom(name), target) + discard window.dispatchEvent(event, target) + proc parseColor(element: Element; s: string): ARGBColor = let cval = parseComponentValue(s) #TODO return element style @@ -2685,10 +2764,7 @@ func newDocument*(factory: CAtomFactory): Document = return document func newDocument(ctx: JSContext): Document {.jsctor.} = - let global = JS_GetGlobalObject(ctx) - let window = fromJS[Window](ctx, global).get - JS_FreeValue(ctx, global) - return newDocument(window.factory) + return newDocument(ctx.getGlobal().factory) func newDocumentType*(document: Document; name, publicId, systemId: string): DocumentType = @@ -3013,8 +3089,8 @@ proc loadResource(window: Window; image: HTMLImageElement) = ) window.loadingResourcePromises.add(p) -proc reflectEvent(element: Element; target: EventTarget; name: StaticAtom; - ctype, value: string) = +proc reflectEvent(element: Element; target: EventTarget; name, ctype: StaticAtom; + value: string) = let document = element.document let ctx = document.window.jsctx let urls = document.baseURL.serialize(excludepassword = true) @@ -3031,7 +3107,7 @@ proc reflectEvent(element: Element; target: EventTarget; name: StaticAtom; # directly here, but a wrapper function that calls fun. Currently # you can run removeEventListener with element.onclick, that should # not work. - doAssert ctx.addEventListener(target, ctype, fun).isSome + doAssert ctx.addEventListener(target, document.toAtom(ctype), fun).isSome JS_FreeValue(ctx, fun) proc reflectAttr(element: Element; name: CAtom; value: Option[string]) = @@ -3074,12 +3150,12 @@ proc reflectAttr(element: Element; name: CAtom; value: Option[string]) = element.cachedStyle = nil return if name == satOnclick and element.scriptingEnabled: - element.reflectEvent(element, name, "click", value.get("")) + element.reflectEvent(element, name, satClick, value.get("")) return case element.tagType of TAG_BODY: if name == satOnload and element.scriptingEnabled: - element.reflectEvent(element.document.window, name, "load", value.get("")) + element.reflectEvent(element.document.window, name, satLoad, value.get("")) return of TAG_INPUT: let input = HTMLInputElement(element) @@ -4216,19 +4292,23 @@ proc jsReflectGet(ctx: JSContext; this: JSValue; magic: cint): JSValue if element.tagType notin entry.tags: return JS_ThrowTypeError(ctx, "Invalid tag type %s", element.tagType) case entry.t - of rtStr: - let x = toJS(ctx, element.attr(entry.attrname)) - return x - of rtBool: - return toJS(ctx, element.attrb(entry.attrname)) - of rtLong: - return toJS(ctx, element.attrl(entry.attrname).get(entry.i)) - of rtUlong: - return toJS(ctx, element.attrul(entry.attrname).get(entry.u)) - of rtUlongGz: - return toJS(ctx, element.attrulgz(entry.attrname).get(entry.u)) + of rtStr: return ctx.toJS(element.attr(entry.attrname)) + of rtBool: return ctx.toJS(element.attrb(entry.attrname)) + of rtLong: return ctx.toJS(element.attrl(entry.attrname).get(entry.i)) + of rtUlong: return ctx.toJS(element.attrul(entry.attrname).get(entry.u)) + of rtUlongGz: return ctx.toJS(element.attrulgz(entry.attrname).get(entry.u)) of rtFunction: - return JS_GetPropertyStr(ctx, this, cstring($entry.attrname)) + let val = ctx.toJS(entry.attrname) + let atom = JS_ValueToAtom(ctx, val) + var res = JS_NULL + var desc: JSPropertyDescriptor + if JS_GetOwnProperty(ctx, addr desc, this, atom) > 0: + JS_FreeValue(ctx, desc.setter) + JS_FreeValue(ctx, desc.getter) + res = desc.value + JS_FreeValue(ctx, val) + JS_FreeAtom(ctx, atom) + return res proc jsReflectSet(ctx: JSContext; this, val: JSValue; magic: cint): JSValue {.cdecl.} = @@ -4272,10 +4352,12 @@ proc jsReflectSet(ctx: JSContext; this, val: JSValue; magic: cint): JSValue let target = fromJS[EventTarget](ctx, this).get ctx.definePropertyC(this, $entry.attrname, JS_DupValue(ctx, val)) #TODO I haven't checked but this might also be wrong - doAssert ctx.addEventListener(target, entry.ctype, val).isSome + let ctype = ctx.getGlobal().toAtom(entry.ctype) + doAssert ctx.addEventListener(target, ctype, val).isSome return JS_DupValue(ctx, val) func getReflectFunctions(tags: set[TagType]): seq[TabGetSet] = + result = @[] for tag in tags: if tag in TagReflectMap: for i in TagReflectMap[tag]: @@ -4285,7 +4367,6 @@ func getReflectFunctions(tags: set[TagType]): seq[TabGetSet] = set: jsReflectSet, magic: i )) - return result func getElementReflectFunctions(): seq[TabGetSet] = result = @[] @@ -4455,13 +4536,13 @@ proc insertAdjacentHTML(element: Element; position, text: string): proc registerElements(ctx: JSContext; nodeCID: JSClassID) = let elementCID = ctx.registerType(Element, parent = nodeCID) - const extra_getset = getElementReflectFunctions() + const extraGetSet = getElementReflectFunctions() let htmlElementCID = ctx.registerType(HTMLElement, parent = elementCID, - has_extra_getset = true, extra_getset = extra_getset) + hasExtraGetSet = true, extraGetSet = extraGetSet) template register(t: typed; tags: set[TagType]) = - const extra_getset = getReflectFunctions(tags) + const extraGetSet = getReflectFunctions(tags) ctx.registerType(t, parent = htmlElementCID, - has_extra_getset = true, extra_getset = extra_getset) + hasExtraGetSet = true, extraGetSet = extra_getset) template register(t: typed; tag: TagType) = register(t, {tag}) register(HTMLInputElement, TAG_INPUT) diff --git a/src/html/env.nim b/src/html/env.nim index 6b70c74f..6f994369 100644 --- a/src/html/env.nim +++ b/src/html/env.nim @@ -109,6 +109,9 @@ proc fetch[T: JSRequest|string](window: Window; input: T; return ok(promise) return ok(window.loader.fetch(input.request)) +# Forward declaration hack +windowFetch = fetch + proc setTimeout(window: Window; handler: JSValue; timeout = 0i32): int32 {.jsfunc.} = return window.timeouts.setTimeout(ttTimeout, handler, timeout) @@ -178,7 +181,7 @@ proc setOnLoad(ctx: JSContext; window: Window; val: JSValue) let this = ctx.toJS(window) ctx.definePropertyC(this, "onload", JS_DupValue(ctx, val)) #TODO I haven't checked but this might also be wrong - doAssert ctx.addEventListener(window, "load", val).isSome + doAssert ctx.addEventListener(window, window.toAtom(satLoad), val).isSome JS_FreeValue(ctx, this) proc addWindowModule*(ctx: JSContext) = @@ -237,8 +240,8 @@ proc runJSJobs*(window: Window) = ctx.writeException(window.console.err) proc newWindow*(scripting, images, styling: bool; selector: Selector[int]; - attrs: WindowAttributes; factory: CAtomFactory; navigate: proc(url: URL); - loader: FileLoader; url: URL): Window = + attrs: WindowAttributes; factory: CAtomFactory; loader: FileLoader; + url: URL): Window = let err = newDynFileStream(stderr) let window = Window( attrs: attrs, @@ -251,7 +254,6 @@ proc newWindow*(scripting, images, styling: bool; selector: Selector[int]; scripting: scripting, origin: url.origin ), - navigate: navigate, factory: factory ) window.location = window.newLocation() diff --git a/src/html/event.nim b/src/html/event.nim index 62d85b00..3d2a3e3a 100644 --- a/src/html/event.nim +++ b/src/html/event.nim @@ -2,6 +2,7 @@ import std/math import std/options import std/times +import html/catom import monoucha/fromjs import monoucha/javascript import monoucha/jserror @@ -28,7 +29,7 @@ type efDispatch Event* = ref object of RootObj - ctype {.jsget: "type".}: string + ctype* {.jsget: "type".}: CAtom target* {.jsget.}: EventTarget currentTarget* {.jsget.}: EventTarget eventPhase {.jsget.}: uint16 @@ -50,7 +51,7 @@ type EventListenerCallback = JSValue EventListener* = ref object - ctype*: string + ctype*: CAtom callback*: EventListenerCallback capture: bool passive: Option[bool] @@ -68,7 +69,7 @@ jsDestructor(EventTarget) var isDefaultPassive*: proc(eventTarget: EventTarget): bool {.nimcall.} = nil type - EventInit = object of JSDict + EventInit* = object of JSDict bubbles: bool cancelable: bool composed: bool @@ -77,7 +78,7 @@ type detail: JSValue # Event -proc innerEventCreationSteps(event: Event; eventInitDict: EventInit) = +proc innerEventCreationSteps*(event: Event; eventInitDict: EventInit) = event.flags = {efInitialized} #TODO this is probably incorrect? # I think it measures the time since the first fork. not sure though @@ -88,21 +89,20 @@ proc innerEventCreationSteps(event: Event; eventInitDict: EventInit) = event.flags.incl(efComposed) #TODO eventInitDict type -proc newEvent(ctype: string; eventInitDict = EventInit()): +proc newEvent(ctx: JSContext; ctype: CAtom; eventInitDict = EventInit()): JSResult[Event] {.jsctor.} = - let event = Event() + let event = Event(ctype: ctype) event.innerEventCreationSteps(eventInitDict) - event.ctype = ctype return ok(event) -proc newEvent*(ctx: JSContext; ctype: string; target: EventTarget): Event = +proc newEvent*(ctype: CAtom; target: EventTarget): Event = return Event( ctype: ctype, target: target, currentTarget: target ) -proc initialize(this: Event; ctype: string; bubbles, cancelable: bool) = +proc initialize(this: Event; ctype: CAtom; bubbles, cancelable: bool) = this.flags.incl(efInitialized) this.isTrusted = false this.target = nil @@ -110,7 +110,7 @@ proc initialize(this: Event; ctype: string; bubbles, cancelable: bool) = this.bubbles = bubbles this.cancelable = cancelable -proc initEvent(this: Event; ctype: string; bubbles, cancelable: bool) +proc initEvent(this: Event; ctype: CAtom; bubbles, cancelable: bool) {.jsfunc.} = if efDispatch notin this.flags: this.initialize(ctype, bubbles, cancelable) @@ -158,7 +158,7 @@ func composed(this: Event): bool {.jsfget.} = return efComposed in this.flags # CustomEvent -proc newCustomEvent(ctype: string; eventInitDict = CustomEventInit()): +proc newCustomEvent(ctype: CAtom; eventInitDict = CustomEventInit()): JSResult[CustomEvent] {.jsctor.} = let event = CustomEvent() event.innerEventCreationSteps(eventInitDict) @@ -169,7 +169,7 @@ proc newCustomEvent(ctype: string; eventInitDict = CustomEventInit()): proc finalize(rt: JSRuntime; this: CustomEvent) {.jsfin.} = JS_FreeValueRT(rt, this.detail) -proc initCustomEvent(this: CustomEvent; ctype: string; +proc initCustomEvent(this: CustomEvent; ctype: CAtom; bubbles, cancelable: bool; detail: JSValue) {.jsfunc.} = if efDispatch notin this.flags: this.initialize(ctype, bubbles, cancelable) @@ -179,15 +179,16 @@ proc initCustomEvent(this: CustomEvent; ctype: string; proc newEventTarget(): EventTarget {.jsctor.} = return EventTarget() -proc defaultPassiveValue(ctype: string; eventTarget: EventTarget): bool = - if ctype in ["touchstart", "touchmove", "wheel", "mousewheel"]: +proc defaultPassiveValue(ctx: JSContext; ctype: CAtom; + eventTarget: EventTarget): bool = + const check = [satTouchstart, satTouchmove, satWheel, satMousewheel] + if ctx.toStaticAtom(ctype) in check: return true return eventTarget.isDefaultPassive() -proc findEventListener(eventTarget: EventTarget; ctype: string; +proc findEventListener(eventTarget: EventTarget; ctype: CAtom; callback: EventListenerCallback; capture: bool): int = - for i in 0 ..< eventTarget.eventListeners.len: - let it = eventTarget.eventListeners[i] + for i, it in eventTarget.eventListeners: if it.ctype == ctype and it.callback == callback and it.capture == capture: return i return -1 @@ -218,12 +219,13 @@ proc invoke*(ctx: JSContext; listener: EventListener; event: Event): return ret # shared -proc addAnEventListener(target: EventTarget; listener: EventListener) = +proc addAnEventListener(ctx: JSContext; target: EventTarget; + listener: EventListener) = #TODO signals if JS_IsUndefined(listener.callback): return if listener.passive.isNone: - listener.passive = some(defaultPassiveValue(listener.ctype, target)) + listener.passive = some(ctx.defaultPassiveValue(listener.ctype, target)) if target.findEventListener(listener.ctype, listener.callback, listener.capture) == -1: # dedup target.eventListeners.add(listener) @@ -267,7 +269,7 @@ proc flattenMore(ctx: JSContext; options: JSValue): passive = some(x.get) return (capture, once, passive) -proc addEventListener*(ctx: JSContext; eventTarget: EventTarget; ctype: string; +proc addEventListener*(ctx: JSContext; eventTarget: EventTarget; ctype: CAtom; callback: EventListenerCallback; options = JS_UNDEFINED): Err[JSError] {.jsfunc.} = if not JS_IsObject(callback) and not JS_IsNull(callback): @@ -280,11 +282,11 @@ proc addEventListener*(ctx: JSContext; eventTarget: EventTarget; ctype: string; once: once, callback: JS_DupValue(ctx, callback) ) - eventTarget.addAnEventListener(listener) + ctx.addAnEventListener(eventTarget, listener) ok() proc removeEventListener(ctx: JSContext; eventTarget: EventTarget; - ctype: string; callback: EventListenerCallback; + ctype: CAtom; callback: EventListenerCallback; options = JS_UNDEFINED) {.jsfunc.} = let capture = flatten(ctx, options) let i = eventTarget.findEventListener(ctype, callback, capture) diff --git a/src/html/xmlhttprequest.nim b/src/html/xmlhttprequest.nim index 92065ab4..2ab6b88d 100644 --- a/src/html/xmlhttprequest.nim +++ b/src/html/xmlhttprequest.nim @@ -1,17 +1,24 @@ import std/options import std/strutils +import std/tables +import html/catom import html/dom import html/event +import io/promise import js/domexception import loader/headers +import loader/loader import loader/request import loader/response import monoucha/fromjs import monoucha/javascript +import monoucha/jserror import monoucha/quickjs +import monoucha/tojs import types/opt import types/url +import utils/twtstr type XMLHttpRequestResponseType = enum @@ -23,47 +30,62 @@ type xhrtText = "text" XMLHttpRequestState = enum - UNSENT = 0u16 - OPENED = 1u16 - HEADERS_RECEIVED = 2u16 - LOADING = 3u16 - DONE = 4u16 + xhrsUnsent = (0u16, "UNSENT") + xhrsOpened = (1u16, "OPENED") + xhrsHeadersReceived = (2u16, "HEADERS_RECEIVED") + xhrsLoading = (3u16, "LOADING") + xhrsDone = (4u16, "DONE") XMLHttpRequestFlag = enum - xhrfSend, xhrfUploadListener, xhrfSync + xhrfSend, xhrfUploadListener, xhrfSync, xhrfUploadComplete, xhrfTimedOut XMLHttpRequestEventTarget = ref object of EventTarget - onloadstart {.jsgetset.}: EventHandler - onprogress {.jsgetset.}: EventHandler - onabort {.jsgetset.}: EventHandler - onerror {.jsgetset.}: EventHandler - onload {.jsgetset.}: EventHandler - ontimeout {.jsgetset.}: EventHandler - onloadend {.jsgetset.}: EventHandler XMLHttpRequestUpload = ref object of XMLHttpRequestEventTarget XMLHttpRequest = ref object of XMLHttpRequestEventTarget - onreadystatechange {.jsgetset.}: EventHandler readyState: XMLHttpRequestState upload {.jsget.}: XMLHttpRequestUpload flags: set[XMLHttpRequestFlag] requestMethod: HttpMethod requestURL: URL - authorRequestHeaders: Headers + headers: Headers response: Response - responseType {.jsgetset.}: XMLHttpRequestResponseType + responseType {.jsget.}: XMLHttpRequestResponseType + timeout {.jsget.}: uint32 + + ProgressEvent = ref object of Event + lengthComputable {.jsget.}: bool + loaded {.jsget.}: uint32 + total {.jsget.}: uint32 + + ProgressEventInit = object of EventInit + lengthComputable: bool + loaded: uint32 + total: uint32 jsDestructor(XMLHttpRequestEventTarget) jsDestructor(XMLHttpRequestUpload) jsDestructor(XMLHttpRequest) +jsDestructor(ProgressEvent) func newXMLHttpRequest(): XMLHttpRequest {.jsctor.} = let upload = XMLHttpRequestUpload() return XMLHttpRequest( upload: upload, - authorRequestHeaders: newHeaders() + headers: newHeaders() + ) + +proc newProgressEvent(ctype: CAtom; init = ProgressEventInit()): ProgressEvent + {.jsctor.} = + let event = ProgressEvent( + ctype: ctype, + lengthComputable: init.lengthComputable, + loaded: init.loaded, + total: init.total ) + Event(event).innerEventCreationSteps(init) + return event func readyState(this: XMLHttpRequest): uint16 {.jsfget.} = return uint16(this.readyState) @@ -82,22 +104,20 @@ proc parseMethod(s: string): DOMResult[HttpMethod] = else: errDOMException("Invalid method", "SyntaxError") -proc open(ctx: JSContext; this: XMLHttpRequest; httpMethod, url: string): - Err[DOMException] {.jsfunc.} = +#TODO the standard says that no async should be treated differently from +# undefined. idk if (and where) this actually matters. +proc open(ctx: JSContext; this: XMLHttpRequest; httpMethod, url: string; + async = true; username = ""; password = ""): Err[DOMException] {.jsfunc.} = let httpMethod = ?parseMethod(httpMethod) - let global = JS_GetGlobalObject(ctx) - let window = fromJS[Window](ctx, global) - JS_FreeValue(ctx, global) - let x = if window.isSome: - parseURL(url, some(window.get.document.baseURL)) - else: - parseURL(url) + let global = ctx.getGlobal() + let x = parseURL(url, some(global.document.baseURL)) if x.isNone: return errDOMException("Invalid URL", "SyntaxError") let parsedURL = x.get - #TODO async, username, password arguments - let async = true - #TODO if async is false... probably just throw. + if not async and ctx.getWindow() != nil and + (this.timeout != 0 or this.responseType != xhrtUnknown): + return errDOMException("Today's horoscope: don't go outside", + "InvalidAccessError") #TODO terminate fetch controller this.flags.excl(xhrfSend) this.flags.excl(xhrfUploadListener) @@ -106,14 +126,235 @@ proc open(ctx: JSContext; this: XMLHttpRequest; httpMethod, url: string): else: this.flags.incl(xhrfSync) this.requestMethod = httpMethod - this.authorRequestHeaders = newHeaders() + this.headers = newHeaders() this.response = makeNetworkError() this.requestURL = parsedURL + #TODO response object, received bytes + if this.readyState != xhrsOpened: + this.readyState = xhrsOpened + global.fireEvent(satReadystatechange, this) return ok() +proc checkOpened(this: XMLHttpRequest): DOMResult[void] = + if this.readyState != xhrsOpened: + return errDOMException("ready state was expected to be `opened'", + "InvalidStateError") + ok() + +proc checkSendFlag(this: XMLHttpRequest): DOMResult[void] = + if xhrfSend in this.flags: + return errDOMException("`send' flag is set", "InvalidStateError") + ok() + +proc setRequestHeader(this: XMLHttpRequest; name, value: string): + DOMResult[void] {.jsfunc.} = + ?this.checkOpened() + ?this.checkSendFlag() + if not name.isValidHeaderName() or not value.isValidHeaderValue(): + return errDOMException("Invalid header name or value", "SyntaxError") + if isForbiddenHeader(name, value): + return ok() + this.headers.table[name.toHeaderCase()] = @[value] + ok() + +proc fireProgressEvent(window: Window; target: EventTarget; name: StaticAtom; + loaded, length: uint32) = + let event = newProgressEvent(window.factory.toAtom(name), ProgressEventInit( + loaded: loaded, + total: length, + lengthComputable: length != 0 + )) + discard window.dispatchEvent(event, target) + +# Forward declaration hack +var windowFetch* {.compileTime.}: proc(window: Window; input: JSRequest; + init = none(RequestInit)): JSResult[FetchPromise] {.nimcall.} = nil + +proc errorSteps(window: Window; this: XMLHttpRequest; name: StaticAtom) = + this.readyState = xhrsDone + this.response = makeNetworkError() + this.flags.excl(xhrfSend) + #TODO sync? + window.fireEvent(satReadystatechange, this) + if xhrfUploadComplete notin this.flags: + this.flags.incl(xhrfUploadComplete) + if xhrfUploadListener in this.flags: + window.fireProgressEvent(this.upload, name, 0, 0) + window.fireProgressEvent(this.upload, satLoadend, 0, 0) + window.fireProgressEvent(this, name, 0, 0) + window.fireProgressEvent(this, satLoadend, 0, 0) + +proc handleErrors(window: Window; this: XMLHttpRequest): DOMException = + if xhrfSend notin this.flags: + return nil + if xhrfTimedOut in this.flags: + window.errorSteps(this, satTimeout) + if xhrfSync in this.flags: + return newDOMException("XHR timed out", "TimeoutError") + elif rfAborted in this.response.flags: + window.errorSteps(this, satAbort) + if xhrfSync in this.flags: + return newDOMException("XHR aborted", "AbortError") + elif this.response.responseType == rtError: + window.errorSteps(this, satError) + if xhrfSync in this.flags: + return newDOMException("Network error in XHR", "NetworkError") + return nil + +proc send(ctx: JSContext; this: XMLHttpRequest; body = JS_NULL): DOMResult[void] + {.jsfunc.} = + ?this.checkOpened() + ?this.checkSendFlag() + var body = body + if this.requestMethod in {hmGet, hmHead}: + body = JS_NULL + let request = newRequest(this.requestURL, this.requestMethod, this.headers) + if not JS_IsNull(body): + let document = fromJS[Document](ctx, body) + if document.isSome: + #TODO replace surrogates + let document = document.get + request.body = RequestBody(t: rbtString, s: document.serializeFragment()) + #TODO else... + if "Content-Type" in this.headers: + request.headers["Content-Type"].setContentTypeAttr("charset", "UTF-8") + elif document.isSome: + request.headers["Content-Type"] = "text/html;charset=UTF-8" + let jsRequest = JSRequest( + #TODO unsafe request flag, client, cors credentials mode, + # use-url-credentials, initiator type + request: request, + mode: rmCors, + credentialsMode: cmSameOrigin, + ) + if JS_IsNull(body): + this.flags.incl(xhrfUploadComplete) + else: + this.flags.excl(xhrfUploadComplete) + this.flags.excl(xhrfTimedOut) + this.flags.incl(xhrfSend) + let window = ctx.getWindow() + if xhrfSync notin this.flags: # async + window.fireProgressEvent(this, satLoadstart, 0, 0) + let p = window.windowFetch(jsRequest) + if p.isSome: + p.get.then(proc(res: JSResult[Response]) = + if res.isNone: + this.response = makeNetworkError() + discard window.handleErrors(this) + return + let response = res.get + this.response = response + this.readyState = xhrsHeadersReceived + window.fireEvent(satReadystatechange, this) + ) + else: # sync + discard #TODO + ok() + +#TODO abort + +proc responseURL(this: XMLHttpRequest): string {.jsfget.} = + return this.response.surl + +proc status(this: XMLHttpRequest): uint16 {.jsfget.} = + return this.response.status + +proc statusText(this: XMLHttpRequest): string {.jsfget.} = + return "" + +proc getResponseHeader(this: XMLHttpRequest; name: string): string {.jsfunc.} = + #TODO ? + return this.response.headers.table.getOrDefault(name)[0] + +#TODO getAllResponseHeaders + +proc setResponseType(ctx: JSContext; this: XMLHttpRequest; + value: XMLHttpRequestResponseType): Err[DOMException] + {.jsfset: "responseType".} = + let window = ctx.getWindow() + if window == nil and value == xhrtDocument: + return ok() + if this.readyState in {xhrsLoading, xhrsDone}: + return errDOMException("readyState must not be loading or done", + "InvalidStateError") + if window != nil and xhrfSync in this.flags: + return errDOMException("responseType may not be set on synchronous XHR", + "InvalidAccessError") + this.responseType = value + ok() + +proc setTimeout(ctx: JSContext; this: XMLHttpRequest; value: uint32): + Err[DOMException] {.jsfset: "timeout".} = + if ctx.getWindow() != nil and xhrfSync in this.flags: + return errDOMException("timeout may not be set on synchronous XHR", + "InvalidAccessError") + this.timeout = value + ok() + +# Event reflection + +const ReflectMap = [ + cint(0): satLoadstart, + satProgress, + satAbort, + satError, + satLoad, + satTimeout, + satLoadend, + satReadystatechange +] + +proc jsReflectGet(ctx: JSContext; this: JSValue; magic: cint): JSValue + {.cdecl.} = + let val = toJS(ctx, $ReflectMap[magic]) + let atom = JS_ValueToAtom(ctx, val) + var res = JS_NULL + var desc: JSPropertyDescriptor + if JS_GetOwnProperty(ctx, addr desc, this, atom) > 0: + JS_FreeValue(ctx, desc.setter) + JS_FreeValue(ctx, desc.getter) + res = JS_GetProperty(ctx, this, atom) + JS_FreeValue(ctx, val) + JS_FreeAtom(ctx, atom) + return res + +proc jsReflectSet(ctx: JSContext; this, val: JSValue; magic: cint): JSValue + {.cdecl.} = + if JS_IsFunction(ctx, val): + let atom = ReflectMap[magic] + let target = fromJS[EventTarget](ctx, this).get + ctx.definePropertyC(this, "on" & $atom, JS_DupValue(ctx, val)) + #TODO I haven't checked but this might also be wrong + doAssert ctx.addEventListener(target, ctx.toAtom(atom), val).isSome + return JS_DupValue(ctx, val) + +func xhretGetSet(): seq[TabGetSet] = + result = @[] + for i, it in ReflectMap: + if it == satReadystatechange: + break + result.add(TabGetSet( + name: "on" & $it, + get: jsReflectGet, + set: jsReflectSet, + magic: int16(i) + )) + proc addXMLHttpRequestModule*(ctx: JSContext) = let eventTargetCID = ctx.getClass("EventTarget") - let xhretCID = ctx.registerType(XMLHttpRequestEventTarget, eventTargetCID) + let eventCID = ctx.getClass("Event") + const getset0 = xhretGetSet() + let xhretCID = ctx.registerType(XMLHttpRequestEventTarget, eventTargetCID, + hasExtraGetSet = true, extraGetSet = getset0) ctx.registerType(XMLHttpRequestUpload, xhretCID) - let xhrCID = ctx.registerType(XMLHttpRequest, xhretCID) + ctx.registerType(ProgressEvent, eventCID) + const getset1 = [TabGetSet( + name: "onreadystatechange", + get: jsReflectGet, + set: jsReflectSet, + magic: int16(ReflectMap.high) + )] + let xhrCID = ctx.registerType(XMLHttpRequest, xhretCID, hasExtraGetSet = true, + extraGetSet = getset1) ctx.defineConsts(xhrCID, XMLHttpRequestState, uint16) diff --git a/src/loader/headers.nim b/src/loader/headers.nim index ff7f9ed3..cd69251c 100644 --- a/src/loader/headers.nim +++ b/src/loader/headers.nim @@ -1,3 +1,4 @@ +import std/strutils import std/tables import monoucha/fromjs @@ -98,7 +99,7 @@ proc `[]=`*(headers: Headers; k: static string, v: string) = const k = k.toHeaderCase() headers.table[k] = @[v] -func `[]`*(headers: Headers; k: static string): string = +func `[]`*(headers: Headers; k: static string): var string = const k = k.toHeaderCase() return headers.table[k][0] @@ -113,5 +114,51 @@ func getOrDefault*(headers: Headers; k: static string; default = ""): string = do: return default +const TokenChars = { + '!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^', '_', '`', '|', '~' +} + AsciiAlphaNumeric + +#TODO maybe assert these are valid on insertion? +func isValidHeaderName*(s: string): bool = + return s.len > 0 and AllChars - TokenChars notin s + +func isValidHeaderValue*(s: string): bool = + return s.len == 0 or s[0] notin {' ', '\t'} and s[^1] notin {' ', '\t'} and + '\n' notin s + +func isForbiddenHeader*(name, value: string): bool = + const ForbiddenNames = [ + "Accept-Charset", + "Accept-Encoding", + "Access-Control-Request-Headers", + "Access-Control-Request-Method", + "Connection", + "Content-Length", + "Cookie", + "Cookie2", + "Date", + "DNT", + "Expect", + "Host", + "Keep-Alive", + "Origin", + "Referer", + "Set-Cookie", + "TE", + "Trailer", + "Transfer-Encoding", + "Upgrade", + "Via", + ] + if name in ForbiddenNames: + return true + if name.startsWith("proxy-") or name.startsWith("sec-"): + return true + if name.equalsIgnoreCase("X-HTTP-Method") or + name.equalsIgnoreCase("X-HTTP-Method-Override") or + name.equalsIgnoreCase("X-Method-Override"): + return true # meh + return false + proc addHeadersModule*(ctx: JSContext) = ctx.registerType(Headers) diff --git a/src/loader/response.nim b/src/loader/response.nim index b8d95e36..8b33621e 100644 --- a/src/loader/response.nim +++ b/src/loader/response.nim @@ -21,12 +21,12 @@ import utils/twtstr type ResponseType* = enum - TYPE_DEFAULT = "default" - TYPE_BASIC = "basic" - TYPE_CORS = "cors" - TYPE_ERROR = "error" - TYPE_OPAQUE = "opaque" - TYPE_OPAQUEREDIRECT = "opaqueredirect" + rtDefault = "default" + rtBasic = "basic" + rtCors = "cors" + rtError = "error" + rtOpaque = "opaque" + rtOpaquedirect = "opaqueredirect" #TODO fully implement headers guards HeadersGuard* = enum @@ -36,6 +36,9 @@ type hgResponse = "response" hgNone = "none" + ResponseFlag* = enum + rfAborted + Response* = ref object responseType* {.jsget: "type".}: ResponseType res*: int @@ -52,6 +55,7 @@ type outputId*: int onRead*: proc(response: Response) {.nimcall.} opaque*: RootRef + flags*: set[ResponseFlag] jsDestructor(Response) @@ -69,7 +73,7 @@ func makeNetworkError*(): Response {.jsstfunc: "Response.error".} = #TODO headers immutable return Response( res: 0, - responseType: TYPE_ERROR, + responseType: rtError, status: 0, headers: newHeaders(), headersGuard: hgImmutable, @@ -79,8 +83,8 @@ func makeNetworkError*(): Response {.jsstfunc: "Response.error".} = func sok(response: Response): bool {.jsfget: "ok".} = return response.status in 200u16 .. 299u16 -func surl(response: Response): string {.jsfget: "url".} = - if response.responseType == TYPE_ERROR: +func surl*(response: Response): string {.jsfget: "url".} = + if response.responseType == rtError or response.url == nil: return "" return $response.url @@ -178,8 +182,7 @@ proc onReadBlob(response: Response) = proc blob*(response: Response): Promise[JSResult[Blob]] {.jsfunc.} = if response.bodyUsed: let p = newPromise[JSResult[Blob]]() - let err = JSResult[Blob] - .err(newTypeError("Body has already been consumed")) + let err = JSResult[Blob].err(newTypeError("Body has already been consumed")) p.resolve(err) return p let opaque = BlobOpaque() diff --git a/src/server/buffer.nim b/src/server/buffer.nim index 743fa46a..424c210f 100644 --- a/src/server/buffer.nim +++ b/src/server/buffer.nim @@ -46,7 +46,6 @@ import monoucha/javascript import monoucha/jsregex import monoucha/libregexp import monoucha/quickjs -import monoucha/tojs import types/blob import types/cell import types/color @@ -859,44 +858,6 @@ proc rewind(buffer: Buffer; offset: int; unregister = true): bool = buffer.bytesRead = offset return true -proc setHTML(buffer: Buffer) = - buffer.initDecoder() - let factory = newCAtomFactory() - buffer.factory = factory - let navigate = if buffer.config.scripting: - proc(url: URL) = buffer.navigate(url) - else: - nil - buffer.window = newWindow( - buffer.config.scripting, - buffer.config.images, - buffer.config.styling, - buffer.selector, - buffer.attrs, - factory, - navigate, - buffer.loader, - buffer.url - ) - let confidence = if buffer.config.charsetOverride == CHARSET_UNKNOWN: - ccTentative - else: - ccCertain - buffer.htmlParser = newHTML5ParserWrapper( - buffer.window, - buffer.url, - buffer.factory, - confidence, - buffer.charset - ) - assert buffer.htmlParser.builder.document != nil - const css = staticRead"res/ua.css" - const quirk = css & staticRead"res/quirk.css" - buffer.uastyle = css.parseStylesheet(factory) - buffer.quirkstyle = quirk.parseStylesheet(factory) - buffer.userstyle = parseStylesheet(buffer.config.userstyle, factory) - buffer.document = buffer.htmlParser.builder.document - # As defined in std/selectors: this determines whether kqueue is being used. # On these platforms, we must not close the selector after fork, since kqueue # fds are not inherited after a fork. @@ -1029,17 +990,16 @@ proc clone*(buffer: Buffer; newurl: URL): int {.proxy.} = proc dispatchDOMContentLoadedEvent(buffer: Buffer) = let window = buffer.window - if window == nil or not buffer.config.scripting: - return let ctx = window.jsctx let document = buffer.document - let event = newEvent(ctx, "DOMContentLoaded", document) + let adcl = window.toAtom(satDOMContentLoaded) + let event = newEvent(adcl, document) var called = false var els = document.eventListeners for el in els: if el.removed: continue - if el.ctype == "DOMContentLoaded": + if el.ctype == adcl: let e = ctx.invoke(el, event) if JS_IsException(e): ctx.writeException(buffer.estream) @@ -1052,16 +1012,15 @@ proc dispatchDOMContentLoadedEvent(buffer: Buffer) = proc dispatchLoadEvent(buffer: Buffer) = let window = buffer.window - if window == nil or not buffer.config.scripting: - return let ctx = window.jsctx - let event = newEvent(ctx, "load", window) + let aload = window.toAtom(satLoad) + let event = newEvent(aload, window) var called = false var els = window.eventListeners for el in els: if el.removed: continue - if el.ctype == "load": + if el.ctype == aload: let e = ctx.invoke(el, event) if JS_IsException(e): ctx.writeException(buffer.estream) @@ -1072,42 +1031,6 @@ proc dispatchLoadEvent(buffer: Buffer) = if called: buffer.do_reshape() -type DispatchEventResult = tuple - called: bool - canceled: bool - -proc dispatchEvent(buffer: Buffer; ctype, jsName: string; elem: Element): - DispatchEventResult = - var called = false - var canceled = false - let ctx = buffer.window.jsctx - let event = newEvent(ctx, ctype, elem) - var jsEvent = ctx.toJS(event) - let jsNameAtom = JS_NewAtomLen(ctx, cstring(jsName), csize_t(jsName.len)) - for a in elem.branch: - event.currentTarget = a - var stop = false - var els = a.eventListeners - for el in els: - if el.ctype == ctype: - let e = ctx.invoke(el, event) - called = true - if JS_IsException(e): - ctx.writeException(buffer.estream) - JS_FreeValue(ctx, e) - if efStopImmediatePropagation in event.flags: - stop = true - break - if efStopPropagation in event.flags: - stop = true - if efCanceled in event.flags: - canceled = true - if stop: - break - JS_FreeValue(ctx, jsEvent) - JS_FreeAtom(ctx, jsNameAtom) - return (called, canceled) - proc finishLoad(buffer: Buffer): EmptyPromise = if buffer.state != bsLoadingPage: let p = EmptyPromise() @@ -1122,7 +1045,8 @@ proc finishLoad(buffer: Buffer): EmptyPromise = )) buffer.htmlParser.finish() buffer.document.readyState = rsInteractive - buffer.dispatchDOMContentLoadedEvent() + if buffer.config.scripting: + buffer.dispatchDOMContentLoadedEvent() buffer.selector.unregister(buffer.fd) buffer.loader.unregistered.add(buffer.fd) buffer.loader.removeCachedItem(buffer.cacheId) @@ -1192,7 +1116,8 @@ proc onload(buffer: Buffer) = buffer.do_reshape() buffer.state = bsLoaded buffer.document.readyState = rsComplete - buffer.dispatchLoadEvent() + if buffer.config.scripting: + buffer.dispatchLoadEvent() if buffer.hasTask(bcGetTitle): buffer.resolveTask(bcGetTitle, buffer.document.title) if buffer.hasTask(bcLoad): @@ -1641,8 +1566,9 @@ proc click*(buffer: Buffer; cursorx, cursory: int): ClickResult {.proxy.} = var canceled = false let clickable = buffer.getCursorClickable(cursorx, cursory) if buffer.config.scripting: - let elem = buffer.getCursorElement(cursorx, cursory) - (called, canceled) = buffer.dispatchEvent("click", "onclick", elem) + let element = buffer.getCursorElement(cursorx, cursory) + let event = newEvent(buffer.window.toAtom(satClick), element) + (called, canceled) = buffer.window.dispatchEvent(event, element) if called: buffer.do_reshape() if not canceled: @@ -1912,6 +1838,11 @@ proc launchBuffer*(config: BufferConfig; url: URL; attrs: WindowAttributes; ssock: ServerSocket; pstream: SocketStream; selector: Selector[int]) = let emptySel = Selector[int]() emptySel[] = selector[] + let factory = newCAtomFactory() + let confidence = if config.charsetOverride == CHARSET_UNKNOWN: + ccTentative + else: + ccCertain let buffer = Buffer( attrs: attrs, config: config, @@ -1927,8 +1858,13 @@ proc launchBuffer*(config: BufferConfig; url: URL; attrs: WindowAttributes; charsetStack: charsetStack, cacheId: -1, outputId: -1, - emptySel: emptySel + emptySel: emptySel, + factory: factory, + window: newWindow(config.scripting, config.images, config.styling, selector, + attrs, factory, loader, url) ) + if buffer.config.scripting: + buffer.window.navigate = proc(url: URL) = buffer.navigate(url) buffer.charset = buffer.charsetStack.pop() var r = pstream.initPacketReader() r.sread(buffer.loader.key) @@ -1943,7 +1879,21 @@ proc launchBuffer*(config: BufferConfig; url: URL; attrs: WindowAttributes; loader.unregisterFun = proc(fd: int) = buffer.selector.unregister(fd) buffer.selector.registerHandle(buffer.rfd, {Read}, 0) - buffer.setHTML() + const css = staticRead"res/ua.css" + const quirk = css & staticRead"res/quirk.css" + buffer.initDecoder() + buffer.uastyle = css.parseStylesheet(factory) + buffer.quirkstyle = quirk.parseStylesheet(factory) + buffer.userstyle = parseStylesheet(buffer.config.userstyle, factory) + buffer.htmlParser = newHTML5ParserWrapper( + buffer.window, + buffer.url, + buffer.factory, + confidence, + buffer.charset + ) + assert buffer.htmlParser.builder.document != nil + buffer.document = buffer.htmlParser.builder.document buffer.runBuffer() buffer.cleanup() quit(0) diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim index 3744d9e2..a4345251 100644 --- a/src/utils/twtstr.nim +++ b/src/utils/twtstr.nim @@ -612,6 +612,33 @@ proc getContentTypeAttr*(contentType, attrname: string): string = s &= c return s +proc setContentTypeAttr*(contentType: var string; attrname, value: string) = + var i = contentType.find(';') + if i == -1: + contentType &= ';' & attrname & '=' & value + return + i = contentType.find(attrname, i) + if i == -1: + contentType &= ';' & attrname & '=' & value + return + i = contentType.skipBlanks(i + attrname.len) + if i >= contentType.len or contentType[i] != '=': + contentType &= ';' & attrname & '=' & value + return + i = contentType.skipBlanks(i + 1) + var q = false + var j = i + while j < contentType.len: + let c = contentType[j] + if q: + q = false + elif c == '\\': + q = true + elif c in AsciiWhitespace + {';'}: + break + inc j + contentType[i..<j] = value + func atob(c: char): uint8 {.inline.} = # see RFC 4648 table if c in AsciiUpperAlpha: diff --git a/test/js/click_setter.html b/test/js/click_setter.html new file mode 100644 index 00000000..125c8490 --- /dev/null +++ b/test/js/click_setter.html @@ -0,0 +1,13 @@ +<!doctype html> +<title>onclick setter/getter</title> +<div id=x>Fail</div> +<script src=asserts.js></script> +<script> +assert(document.getElementById("x").onclick === null); +function myFunction() { + this.textContent = "hello" +} +document.getElementById("x").onclick = myFunction; +assert(myFunction == document.getElementById("x").onclick); +document.getElementById("x").textContent = "Success"; +</script> diff --git a/test/js/xhr.html b/test/js/xhr.html new file mode 100644 index 00000000..4bd66b95 --- /dev/null +++ b/test/js/xhr.html @@ -0,0 +1,17 @@ +<!doctype html> +<title>onclick setter/getter</title> +<div id=x>Fail</div> +<script src=asserts.js></script> +<script> +const x = new XMLHttpRequest(); +assert(x.onreadystatechange === null); +function myFunction() { + console.log("change"); +} +x.onreadystatechange = myFunction; +assert(myFunction === x.onreadystatechange); +assert(x.readyState === XMLHttpRequest.UNSENT); +x.open("GET", ""); +document.getElementById("x").textContent = "Success"; +x.send(); +</script> |