diff options
author | bptato <nincsnevem662@gmail.com> | 2022-09-16 00:30:56 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2022-09-16 00:30:56 +0200 |
commit | 3f96681261692feedadfb8c488f3908dd80bb01c (patch) | |
tree | b1c1b809ab18db62e641e8141b05053081c8a204 /src | |
parent | cea4aa0374a2f8c1df40a30060c34de8ce257090 (diff) | |
download | chawan-3f96681261692feedadfb8c488f3908dd80bb01c.tar.gz |
Bugfixes & test JS event loop
Diffstat (limited to 'src')
-rw-r--r-- | src/display/client.nim | 128 | ||||
-rw-r--r-- | src/html/dom.nim | 50 | ||||
-rw-r--r-- | src/js/javascript.nim | 90 |
3 files changed, 214 insertions, 54 deletions
diff --git a/src/display/client.nim b/src/display/client.nim index efdc2c88..ef855017 100644 --- a/src/display/client.nim +++ b/src/display/client.nim @@ -2,10 +2,13 @@ import options import os import streams import strutils +import tables import terminal import times import unicode +import std/monotimes + import css/sheet import config/config import html/dom @@ -39,6 +42,13 @@ type needsauth: bool redirecturl: Option[Url] cmdmode: bool + timeoutid: int + timeouts: Table[int, tuple[handler: proc(), time: int64]] + added_timeouts: Table[int, tuple[handler: proc(), time: int64]] + removed_timeouts: seq[int] + intervals: Table[int, tuple[handler: proc(), time: int64, wait: int, del: JSValue]] + added_intervals: Table[int, tuple[handler: proc(), time: int64, wait: int, del: JSValue]] + removed_intervals: seq[int] Console* = ref object err*: Stream @@ -397,25 +407,25 @@ proc evalJS(client: Client, src, filename: string): JSObject = unblockStdin() return client.jsctx.eval(src, filename, JS_EVAL_TYPE_GLOBAL) -proc command0(client: Client, src: string, filename = "<command>") = +proc evalJSFree(client: Client, src, filename: string) = + free(client.evalJS(src, filename)) + +proc command0(client: Client, src: string, filename = "<command>", silence = false) = let ret = client.evalJS(src, filename) if ret.isException(): - let ex = client.jsctx.getException() - let str = ex.toString() - if str.issome: - client.console.err.write(str.get & '\n') - var stack = ex.getProperty("stack") - if not stack.isUndefined(): - let str = stack.toString() - if str.issome: - client.console.err.write(str.get) - free(stack) - free(ex) + client.jsctx.writeException(client.console.err) else: - let str = ret.toString() - if str.issome: - client.console.err.write(str.get & '\n') + if not silence: + let str = ret.toString() + if str.issome: + client.console.err.write(str.get & '\n') free(ret) + for k, v in client.added_timeouts: + client.timeouts[k] = v + client.added_timeouts.clear() + for k, v in client.added_intervals: + client.intervals = client.added_intervals + client.added_intervals.clear() proc command(client: Client, src: string) = restoreStdin() @@ -609,7 +619,7 @@ proc input(client: Client) = client.s &= c let action = getNormalAction(client.s) - discard client.evalJS(action, "<command>") + client.evalJSFree(action, "<command>") proc followRedirect(client: Client) @@ -675,12 +685,94 @@ proc readFile(client: Client, path: string): string {.jsfunc.} = proc writeFile(client: Client, path: string, content: string) {.jsfunc.} = writeFile(path, content) +import bindings/quickjs + +proc setTimeout[T: JSObject|string](client: Client, handler: T, timeout = 0): int {.jsfunc.} = + let id = client.timeoutid + inc client.timeoutid + when T is string: + client.added_timeouts[id] = ((proc() = + client.evalJSFree(handler, "setTimeout handler") + ), getMonoTime().ticks div 1_000_000 + timeout) + else: + let fun = JS_DupValue(handler.ctx, handler.val) + client.added_timeouts[id] = ((proc() = + let ret = JSObject(ctx: handler.ctx, val: fun).callFunction() + if ret.isException(): + ret.ctx.writeException(client.console.err) + JS_FreeValue(ret.ctx, ret.val) + JS_FreeValue(ret.ctx, fun) + ), getMonoTime().ticks div 1_000_000 + timeout) + return id + +proc setInterval[T: JSObject|string](client: Client, handler: T, interval = 0): int {.jsfunc.} = + let id = client.timeoutid + inc client.timeoutid + when T is string: + client.added_intervals[id] = ((proc() = + client.evalJSFree(handler, "setInterval handler") + ), getMonoTime().ticks div 1_000_000 + interval, interval, JS_NULL) + else: + let fun = JS_DupValue(handler.ctx, handler.val) + client.added_intervals[id] = ((proc() = + let ret = JSObject(ctx: handler.ctx, val: fun).callFunction() + if ret.isException(): + ret.ctx.writeException(client.console.err) + JS_FreeValue(ret.ctx, ret.val) + ), getMonoTime().ticks div 1_000_000 + interval, interval, fun) + return id + +proc clearTimeout(client: Client, id: int) {.jsfunc.} = + client.removed_timeouts.add(id) + +proc clearInterval(client: Client, id: int) {.jsfunc.} = + client.removed_intervals.add(id) + +proc jsEventLoop(client: Client) = + while client.timeouts.len > 0 or client.intervals.len > 0: + var wait = -1 + let curr = getMonoTime().ticks div 1_000_000 + for k, v in client.timeouts: + if v.time <= curr: + v.handler() + client.removed_timeouts.add(k) + for k, v in client.intervals.mpairs: + if v.time <= curr: + v.handler() + v.time = curr + v.wait + for k, v in client.added_timeouts: + client.timeouts[k] = v + client.added_timeouts.clear() + for k, v in client.added_intervals: + client.intervals[k] = v + client.added_intervals.clear() + for k in client.removed_timeouts: + client.timeouts.del(k) + for k in client.removed_intervals: + if k in client.intervals and client.intervals[k].del != JS_NULL: + JS_FreeValue(client.jsctx, client.intervals[k].del) + client.intervals.del(k) + client.removed_timeouts.setLen(0) + client.removed_intervals.setLen(0) + for k, v in client.timeouts: + if wait != -1: + wait = min(wait, int(v.time - curr)) + else: + wait = int(v.time - curr) + for k, v in client.intervals: + if wait != -1: + wait = min(wait, int(v.time - curr)) + else: + wait = int(v.time - curr) + if wait > 0: + sleep(wait) + proc launchClient*(client: Client, pages: seq[string], ctype: string, dump: bool) = if gconfig.startup != "": let s = readFile(gconfig.startup) - #client.command(s) client.console.err = newFileStream(stderr) - client.command0(s, gconfig.startup) + client.command0(s, gconfig.startup, silence = true) + client.jsEventLoop() client.console.err = newStringStream() quit() client.userstyle = gconfig.stylesheet.parseStylesheet() diff --git a/src/html/dom.nim b/src/html/dom.nim index 9c0f0a59..278a4c5e 100644 --- a/src/html/dom.nim +++ b/src/html/dom.nim @@ -38,8 +38,8 @@ type childNodes* {.jsget.}: seq[Node] nextSibling*: Node previousSibling*: Node - parentNode*: Node - parentElement*: Element + parentNode* {.jsget.}: Node + parentElement* {.jsget.}: Element root: Node document*: Document @@ -164,18 +164,41 @@ type ctype*: bool #TODO result -# For debugging proc tostr(ftype: enum): string = return ($ftype).split('_')[1..^1].join("-").tolower() +func escapeText(s: string, attribute_mode = false): string = + var nbsp_mode = false + var nbsp_prev: char + for c in s: + if nbsp_mode: + if c == char(0xA0): + result &= " " + else: + result &= nbsp_prev & c + nbsp_mode = false + elif c == '&': + result &= "&" + elif c == char(0xC2): + nbsp_mode = true + nbsp_prev = c + elif attribute_mode and c == '"': + result &= """ + elif not attribute_mode and c == '<': + result &= "<" + elif not attribute_mode and c == '>': + result &= ">" + else: + result &= c + func `$`*(node: Node): string = - if node == nil: return "nil" + if node == nil: return "nil" #TODO this isn't standard compliant but helps debugging case node.nodeType of ELEMENT_NODE: let element = Element(node) result = "<" & $element.tagType.tostr() for k, v in element.attributes: - result &= ' ' & k & (if v != "": "=\"" & v & "\"" else: "") + result &= ' ' & k & "=\"" & v.escapeText(true) & "\"" result &= ">\n" for node in element.childNodes: for line in ($node).split('\n'): @@ -183,9 +206,13 @@ func `$`*(node: Node): string = result &= "</" & $element.tagType.tostr() & ">" of TEXT_NODE: let text = Text(node) - result = text.data + result = text.data.escapeText() of COMMENT_NODE: result = "<!-- " & Comment(node).data & "-->" + of PROCESSING_INSTRUCTION_NODE: + result = "" #TODO + of DOCUMENT_TYPE_NODE: + result = "<!DOCTYPE" & ' ' & DocumentType(node).name & ">" else: result = "Node of " & $node.nodeType @@ -222,11 +249,11 @@ iterator branch*(node: Node): Node {.inline.} = # Returns the node's descendants iterator descendants*(node: Node): Node {.inline.} = var stack: seq[Node] - stack.add(node.childNodes) + stack.add(node) while stack.len > 0: let node = stack.pop() - yield node for i in countdown(node.childNodes.high, 0): + yield node.childNodes[i] stack.add(node.childNodes[i]) iterator elements*(node: Node): Element {.inline.} = @@ -421,6 +448,13 @@ func attrb*(element: Element, s: string): bool = return true return false +func innerHTML*(element: Element): string {.jsget.} = + for child in element.childNodes: + result &= $child + +func outerHTML*(element: Element): string {.jsget.} = + return $element + func textContent*(node: Node): string {.jsget.} = case node.nodeType of DOCUMENT_NODE, DOCUMENT_TYPE_NODE: diff --git a/src/js/javascript.nim b/src/js/javascript.nim index 0bb7b12f..4ed14212 100644 --- a/src/js/javascript.nim +++ b/src/js/javascript.nim @@ -1,5 +1,6 @@ import macros import options +import streams import strformat import strutils import tables @@ -106,7 +107,7 @@ func getJSObject*(ctx: JSContext, v: JSValue): JSObject = result.ctx = ctx result.val = v -func getJSValue*(ctx: JSContext, argv: ptr JSValue, i: int): JSValue = +func getJSValue(ctx: JSContext, argv: ptr JSValue, i: int): JSValue {.inline.} = cast[ptr JSValue](cast[int](argv) + i * sizeof(JSValue))[] func newJSObject*(ctx: JSContext): JSObject = @@ -222,6 +223,19 @@ func toString*(ctx: JSContext, val: JSValue): Option[string] = result = some(ret) JS_FreeCString(ctx, outp) +proc writeException*(ctx: JSContext, s: Stream) = + let ex = JS_GetException(ctx) + let str = toString(ctx, ex) + if str.issome: + s.write(str.get & '\n') + let stack = JS_GetPropertyStr(ctx, ex, cstring("stack")); + if not JS_IsUndefined(stack): + let str = toString(ctx, stack) + if str.issome: + s.write(str.get) + JS_FreeValue(ctx, stack) + JS_FreeValue(ctx, ex) + func toString*(obj: JSObject): Option[string] = toString(obj.ctx, obj.val) func `$`*(obj: JSObject): string = @@ -293,6 +307,14 @@ func newJSClass*(ctx: JSContext, cdef: JSClassDefConst, cctor: JSCFunction, func ctx.setProperty(global, $cdef.class_name, jctor) JS_FreeValue(ctx, global) +proc callFunction*(fun: JSObject): JSObject = + result.ctx = fun.ctx + result.val = JS_Call(fun.ctx, fun.val, JS_UNDEFINED, 0, nil) + +proc callFunction*(fun: JSObject, this: JSObject): JSObject = + result.ctx = fun.ctx + result.val = JS_Call(fun.ctx, fun.val, this.val, 0, nil) + type FuncParam = tuple[name: string, t: NimNode, val: Option[NimNode], generic: Option[NimNode]] func getMinArgs(params: seq[FuncParam]): int = @@ -540,6 +562,8 @@ proc fromJS[T](ctx: JSContext, val: JSValue): Option[T] = except ValueError: JS_ThrowTypeError(ctx, "`%s' is not a valid value for enumeration %s", cstring(s.get), $T) return none(T) + elif T is JSObject: + return some(JSObject(ctx: ctx, val: val)) elif T is object: #TODO TODO TODO dictionary case return none(T) @@ -807,6 +831,7 @@ proc addUnionParam(gen: var JSFuncGenerator, tt: NimNode, s: NimNode, fallback: var tableg = none(NimNode) var seqg = none(NimNode) var hasString = false + var hasJSObject = false for g in flattened: if g.len > 0 and g[0] == Table.getType(): tableg = some(g) @@ -814,6 +839,8 @@ proc addUnionParam(gen: var JSFuncGenerator, tt: NimNode, s: NimNode, fallback: seqg = some(g) elif g == string.getType(): hasString = true + elif g == JSObject.getTypeInst(): + hasJSObject = true # 4. If V is null or undefined, then: #TODO this is wrong. map dictionary to object instead #if tableg.issome: @@ -827,34 +854,41 @@ proc addUnionParam(gen: var JSFuncGenerator, tt: NimNode, s: NimNode, fallback: # let `s` = Table[`a`, `b`](), # fallback) # 10. If Type(V) is Object, then: - if tableg.issome or seqg.issome: - # Sequence: - if seqg.issome: - let query = quote do: - ( - let o = getJSValue(ctx, argv, `j`) - JS_IsObject(o) and ( - let prop = JS_GetProperty(ctx, o, ctx.getOpaque().sym_iterator) - if JS_IsException(prop): - return JS_EXCEPTION - let ret = not JS_IsUndefined(prop) - JS_FreeValue(ctx, prop) - ret - ) + # Sequence: + if seqg.issome: + let query = quote do: + ( + let o = getJSValue(ctx, argv, `j`) + JS_IsObject(o) and ( + let prop = JS_GetProperty(ctx, o, ctx.getOpaque().sym_iterator) + if JS_IsException(prop): + return JS_EXCEPTION + let ret = not JS_IsUndefined(prop) + JS_FreeValue(ctx, prop) + ret ) - let a = seqg.get[1] - gen.addUnionParamBranch(query, quote do: - let `s` = fromJS_or_return(seq[`a`], ctx, getJSValue(ctx, argv, `j`)), - fallback) - # Record: - if tableg.issome: - let a = tableg.get[1] - let b = tableg.get[2] - let query = quote do: - JS_IsObject(getJSValue(ctx, argv, `j`)) - gen.addUnionParamBranch(query, quote do: - let `s` = fromJS_or_return(Table[`a`, `b`], ctx, getJSValue(ctx, argv, `j`)), - fallback) + ) + let a = seqg.get[1] + gen.addUnionParamBranch(query, quote do: + let `s` = fromJS_or_return(seq[`a`], ctx, getJSValue(ctx, argv, `j`)), + fallback) + # Record: + if tableg.issome: + let a = tableg.get[1] + let b = tableg.get[2] + let query = quote do: + JS_IsObject(getJSValue(ctx, argv, `j`)) + gen.addUnionParamBranch(query, quote do: + let `s` = fromJS_or_return(Table[`a`, `b`], ctx, getJSValue(ctx, argv, `j`)), + fallback) + # Object (JSObject variant): + #TODO non-JS objects + if hasJSObject: + let query = quote do: + JS_IsObject(getJSValue(ctx, argv, `j`)) + gen.addUnionParamBranch(query, quote do: + let `s` = fromJS_or_return(JSObject, ctx, getJSValue(ctx, argv, `j`)), + fallback) # 14. If types includes a string type, then return the result of converting V # to that type. |