about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2022-09-16 00:30:56 +0200
committerbptato <nincsnevem662@gmail.com>2022-09-16 00:30:56 +0200
commit3f96681261692feedadfb8c488f3908dd80bb01c (patch)
treeb1c1b809ab18db62e641e8141b05053081c8a204
parentcea4aa0374a2f8c1df40a30060c34de8ce257090 (diff)
downloadchawan-3f96681261692feedadfb8c488f3908dd80bb01c.tar.gz
Bugfixes & test JS event loop
-rw-r--r--src/display/client.nim128
-rw-r--r--src/html/dom.nim50
-rw-r--r--src/js/javascript.nim90
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 &= "&nbsp;"
+      else:
+        result &= nbsp_prev & c
+      nbsp_mode = false
+    elif c == '&':
+      result &= "&amp;"
+    elif c == char(0xC2):
+      nbsp_mode = true
+      nbsp_prev = c
+    elif attribute_mode and c == '"':
+      result &= "&quot;"
+    elif not attribute_mode and c == '<':
+      result &= "&lt;"
+    elif not attribute_mode and c == '>':
+      result &= "&gt;"
+    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.