summary refs log tree commit diff stats
path: root/lib/js/jsffi.nim
diff options
context:
space:
mode:
Diffstat (limited to 'lib/js/jsffi.nim')
-rw-r--r--lib/js/jsffi.nim527
1 files changed, 527 insertions, 0 deletions
diff --git a/lib/js/jsffi.nim b/lib/js/jsffi.nim
new file mode 100644
index 000000000..d50d58ae5
--- /dev/null
+++ b/lib/js/jsffi.nim
@@ -0,0 +1,527 @@
+#
+#
+#            Nim's Runtime Library
+#        (c) Copyright 2017 Nim Authors
+#
+#    See the file "copying.txt", included in this
+#    distribution, for details about the copyright.
+#
+
+## This Module implements types and macros to facilitate the wrapping of, and
+## interaction with JavaScript libraries. Using the provided types `JsObject`
+## and `JsAssoc` together with the provided macros allows for smoother
+## interfacing with JavaScript, allowing for example quick and easy imports of
+## JavaScript variables:
+
+runnableExamples:
+  # Here, we are using jQuery for just a few calls and do not want to wrap the
+  # whole library:
+
+  # import the document object and the console
+  var document {.importc, nodecl.}: JsObject
+  var console {.importc, nodecl.}: JsObject
+  # import the "$" function
+  proc jq(selector: JsObject): JsObject {.importjs: "$$(#)".}
+
+  # Use jQuery to make the following code run, after the document is ready.
+  # This uses an experimental `.()` operator for `JsObject`, to emit
+  # JavaScript calls, when no corresponding proc exists for `JsObject`.
+  proc main =
+    jq(document).ready(proc() =
+      console.log("Hello JavaScript!")
+    )
+
+
+when not defined(js) and not defined(nimsuggest):
+  {.fatal: "Module jsFFI is designed to be used with the JavaScript backend.".}
+
+import std/[macros, tables]
+
+const
+  setImpl = "#[#] = #"
+  getImpl = "#[#]"
+
+var
+  mangledNames {.compileTime.} = initTable[string, string]()
+  nameCounter {.compileTime.} = 0
+
+proc validJsName(name: string): bool =
+  result = true
+  const reservedWords = ["break", "case", "catch", "class", "const", "continue",
+    "debugger", "default", "delete", "do", "else", "export", "extends",
+    "finally", "for", "function", "if", "import", "in", "instanceof", "new",
+    "return", "super", "switch", "this", "throw", "try", "typeof", "var",
+    "void", "while", "with", "yield", "enum", "implements", "interface",
+    "let", "package", "private", "protected", "public", "static", "await",
+    "abstract", "boolean", "byte", "char", "double", "final", "float", "goto",
+    "int", "long", "native", "short", "synchronized", "throws", "transient",
+    "volatile", "null", "true", "false"]
+  case name
+  of reservedWords: return false
+  else: discard
+  if name[0] notin {'A'..'Z','a'..'z','_','$'}: return false
+  for chr in name:
+    if chr notin {'A'..'Z','a'..'z','_','$','0'..'9'}:
+      return false
+
+template mangleJsName(name: string): string =
+  inc nameCounter
+  "mangledName" & $nameCounter
+
+# only values that can be mapped 1 to 1 with cstring should be keys: they have an injective function with cstring
+
+proc toJsKey*[T: SomeInteger](text: cstring, t: type T): T {.importjs: "parseInt(#)".}
+
+proc toJsKey*[T: enum](text: cstring, t: type T): T =
+  T(text.toJsKey(int))
+
+proc toJsKey*(text: cstring, t: type cstring): cstring =
+  text
+
+proc toJsKey*[T: SomeFloat](text: cstring, t: type T): T {.importjs: "parseFloat(#)".}
+
+type
+  JsKey* = concept a, type T
+    cstring.toJsKey(T) is T
+
+  JsObject* = ref object of JsRoot
+    ## Dynamically typed wrapper around a JavaScript object.
+  JsAssoc*[K: JsKey, V] = ref object of JsRoot
+    ## Statically typed wrapper around a JavaScript object.
+
+  js* = JsObject
+
+var
+  jsArguments* {.importc: "arguments", nodecl}: JsObject
+    ## JavaScript's arguments pseudo-variable.
+  jsNull* {.importc: "null", nodecl.}: JsObject
+    ## JavaScript's null literal.
+  jsUndefined* {.importc: "undefined", nodecl.}: JsObject
+    ## JavaScript's undefined literal.
+  jsDirname* {.importc: "__dirname", nodecl.}: cstring
+    ## JavaScript's __dirname pseudo-variable.
+  jsFilename* {.importc: "__filename", nodecl.}: cstring
+    ## JavaScript's __filename pseudo-variable.
+
+proc isNull*[T](x: T): bool {.noSideEffect, importjs: "(# === null)".}
+  ## Checks if a value is exactly null.
+
+proc isUndefined*[T](x: T): bool {.noSideEffect, importjs: "(# === undefined)".}
+  ## Checks if a value is exactly undefined.
+
+# Exceptions
+type
+  JsError* {.importc: "Error".} = object of JsRoot
+    message*: cstring
+  JsEvalError* {.importc: "EvalError".} = object of JsError
+  JsRangeError* {.importc: "RangeError".} = object of JsError
+  JsReferenceError* {.importc: "ReferenceError".} = object of JsError
+  JsSyntaxError* {.importc: "SyntaxError".} = object of JsError
+  JsTypeError* {.importc: "TypeError".} = object of JsError
+  JsURIError* {.importc: "URIError".} = object of JsError
+
+# New
+proc newJsObject*: JsObject {.importjs: "{@}".}
+  ## Creates a new empty JsObject.
+
+proc newJsAssoc*[K: JsKey, V]: JsAssoc[K, V] {.importjs: "{@}".}
+  ## Creates a new empty JsAssoc with key type `K` and value type `V`.
+
+# Checks
+proc hasOwnProperty*(x: JsObject, prop: cstring): bool
+  {.importjs: "#.hasOwnProperty(#)".}
+  ## Checks, whether `x` has a property of name `prop`.
+
+proc jsTypeOf*(x: JsObject): cstring {.importjs: "typeof(#)".}
+  ## Returns the name of the JsObject's JavaScript type as a cstring.
+
+proc jsNew*(x: auto): JsObject {.importjs: "(new #)".}
+  ## Turns a regular function call into an invocation of the
+  ## JavaScript's `new` operator.
+
+proc jsDelete*(x: auto): JsObject {.importjs: "(delete #)".}
+  ## JavaScript's `delete` operator.
+
+proc require*(module: cstring): JsObject {.importc.}
+  ## JavaScript's `require` function.
+
+# Conversion to and from JsObject
+proc to*(x: JsObject, T: typedesc): T {.importjs: "(#)".}
+  ## Converts a JsObject `x` to type `T`.
+
+proc toJs*[T](val: T): JsObject {.importjs: "(#)".}
+  ## Converts a value of any type to type JsObject.
+
+template toJs*(s: string): JsObject = cstring(s).toJs
+
+macro jsFromAst*(n: untyped): untyped =
+  result = n
+  if n.kind == nnkStmtList:
+    result = newProc(procType = nnkDo, body = result)
+  return quote: toJs(`result`)
+
+proc `&`*(a, b: cstring): cstring {.importjs: "(# + #)".}
+  ## Concatenation operator for JavaScript strings.
+
+proc `+`*(x, y: JsObject): JsObject {.importjs: "(# + #)".}
+proc `-`*(x, y: JsObject): JsObject {.importjs: "(# - #)".}
+proc `*`*(x, y: JsObject): JsObject {.importjs: "(# * #)".}
+proc `/`*(x, y: JsObject): JsObject {.importjs: "(# / #)".}
+proc `%`*(x, y: JsObject): JsObject {.importjs: "(# % #)".}
+proc `+=`*(x, y: JsObject): JsObject {.importjs: "(# += #)", discardable.}
+proc `-=`*(x, y: JsObject): JsObject {.importjs: "(# -= #)", discardable.}
+proc `*=`*(x, y: JsObject): JsObject {.importjs: "(# *= #)", discardable.}
+proc `/=`*(x, y: JsObject): JsObject {.importjs: "(# /= #)", discardable.}
+proc `%=`*(x, y: JsObject): JsObject {.importjs: "(# %= #)", discardable.}
+proc `++`*(x:    JsObject): JsObject {.importjs: "(++#)".}
+proc `--`*(x:    JsObject): JsObject {.importjs: "(--#)".}
+proc `>`*(x, y: JsObject): JsObject {.importjs: "(# > #)".}
+proc `<`*(x, y: JsObject): JsObject {.importjs: "(# < #)".}
+proc `>=`*(x, y: JsObject): JsObject {.importjs: "(# >= #)".}
+proc `<=`*(x, y: JsObject): JsObject {.importjs: "(# <= #)".}
+proc `**`*(x, y: JsObject): JsObject {.importjs: "((#) ** #)".}
+  # (#) needed, refs https://github.com/nim-lang/Nim/pull/16409#issuecomment-760550812
+proc `and`*(x, y: JsObject): JsObject {.importjs: "(# && #)".}
+proc `or`*(x, y: JsObject): JsObject {.importjs: "(# || #)".}
+proc `not`*(x:    JsObject): JsObject {.importjs: "(!#)".}
+proc `in`*(x, y: JsObject): JsObject {.importjs: "(# in #)".}
+
+proc `[]`*(obj: JsObject, field: cstring): JsObject {.importjs: getImpl.}
+  ## Returns the value of a property of name `field` from a JsObject `obj`.
+
+proc `[]`*(obj: JsObject, field: int): JsObject {.importjs: getImpl.}
+  ## Returns the value of a property of name `field` from a JsObject `obj`.
+
+proc `[]=`*[T](obj: JsObject, field: cstring, val: T) {.importjs: setImpl.}
+  ## Sets the value of a property of name `field` in a JsObject `obj` to `v`.
+
+proc `[]=`*[T](obj: JsObject, field: int, val: T) {.importjs: setImpl.}
+  ## Sets the value of a property of name `field` in a JsObject `obj` to `v`.
+
+proc `[]`*[K: JsKey, V](obj: JsAssoc[K, V], field: K): V
+  {.importjs: getImpl.}
+  ## Returns the value of a property of name `field` from a JsAssoc `obj`.
+
+proc `[]=`*[K: JsKey, V](obj: JsAssoc[K, V], field: K, val: V)
+  {.importjs: setImpl.}
+  ## Sets the value of a property of name `field` in a JsAssoc `obj` to `v`.
+
+proc `[]`*[V](obj: JsAssoc[cstring, V], field: string): V =
+  obj[cstring(field)]
+
+proc `[]=`*[V](obj: JsAssoc[cstring, V], field: string, val: V) =
+  obj[cstring(field)] = val
+
+proc `==`*(x, y: JsRoot): bool {.importjs: "(# === #)".}
+  ## Compares two JsObjects or JsAssocs. Be careful though, as this is comparison
+  ## like in JavaScript, so if your JsObjects are in fact JavaScript Objects,
+  ## and not strings or numbers, this is a *comparison of references*.
+
+{.experimental.}
+macro `.`*(obj: JsObject, field: untyped): JsObject =
+  ## Experimental dot accessor (get) for type JsObject.
+  ## Returns the value of a property of name `field` from a JsObject `x`.
+  runnableExamples:
+    let obj = newJsObject()
+    obj.a = 20
+    assert obj.a.to(int) == 20
+  if validJsName($field):
+    let importString = "#." & $field
+    let helperName = genSym(nskProc, "helper")
+    result = quote do:
+      proc `helperName`(o: JsObject): JsObject
+        {.importjs: `importString`.}
+      `helperName`(`obj`)
+  else:
+    if not mangledNames.hasKey($field):
+      mangledNames[$field] = mangleJsName($field)
+    let importString = "#." & mangledNames[$field]
+    let helperName = genSym(nskProc, "helper")
+    result = quote do:
+      proc `helperName`(o: JsObject): JsObject
+        {.importjs: `importString`.}
+      `helperName`(`obj`)
+
+macro `.=`*(obj: JsObject, field, value: untyped): untyped =
+  ## Experimental dot accessor (set) for type JsObject.
+  ## Sets the value of a property of name `field` in a JsObject `x` to `value`.
+  if validJsName($field):
+    let importString = "#." & $field & " = #"
+    let helperName = genSym(nskProc, "helper")
+    result = quote do:
+      proc `helperName`(o: JsObject, v: auto)
+        {.importjs: `importString`.}
+      `helperName`(`obj`, `value`)
+  else:
+    if not mangledNames.hasKey($field):
+      mangledNames[$field] = mangleJsName($field)
+    let importString = "#." & mangledNames[$field] & " = #"
+    let helperName = genSym(nskProc, "helper")
+    result = quote do:
+      proc `helperName`(o: JsObject, v: auto)
+        {.importjs: `importString`.}
+      `helperName`(`obj`, `value`)
+
+macro `.()`*(obj: JsObject,
+             field: untyped,
+             args: varargs[JsObject, jsFromAst]): JsObject =
+  ## Experimental "method call" operator for type JsObject.
+  ## Takes the name of a method of the JavaScript object (`field`) and calls
+  ## it with `args` as arguments, returning a JsObject (which may be discarded,
+  ## and may be `undefined`, if the method does not return anything,
+  ## so be careful when using this.)
+  ##
+  ## Example:
+  ##   ```nim
+  ##   # Let's get back to the console example:
+  ##   var console {.importc, nodecl.}: JsObject
+  ##   let res = console.log("I return undefined!")
+  ##   console.log(res) # This prints undefined, as console.log always returns
+  ##                    # undefined. Thus one has to be careful, when using
+  ##                    # JsObject calls.
+  ##   ```
+  var importString: string
+  if validJsName($field):
+    importString = "#." & $field & "(@)"
+  else:
+    if not mangledNames.hasKey($field):
+      mangledNames[$field] = mangleJsName($field)
+    importString = "#." & mangledNames[$field] & "(@)"
+  let helperName = genSym(nskProc, "helper")
+  result = quote do:
+    proc `helperName`(o: JsObject): JsObject
+      {.importjs: `importString`, discardable.}
+    `helperName`(`obj`)
+  for idx in 0 ..< args.len:
+    let paramName = newIdentNode("param" & $idx)
+    result[0][3].add newIdentDefs(paramName, newIdentNode("JsObject"))
+    result[1].add args[idx].copyNimTree
+
+macro `.`*[K: cstring, V](obj: JsAssoc[K, V],
+                                   field: untyped): V =
+  ## Experimental dot accessor (get) for type JsAssoc.
+  ## Returns the value of a property of name `field` from a JsObject `x`.
+  var importString: string
+  if validJsName($field):
+    importString = "#." & $field
+  else:
+    if not mangledNames.hasKey($field):
+      mangledNames[$field] = mangleJsName($field)
+    importString = "#." & mangledNames[$field]
+  let helperName = genSym(nskProc, "helper")
+  result = quote do:
+    proc `helperName`(o: type(`obj`)): `obj`.V
+      {.importjs: `importString`.}
+    `helperName`(`obj`)
+
+macro `.=`*[K: cstring, V](obj: JsAssoc[K, V],
+                                    field: untyped,
+                                    value: V): untyped =
+  ## Experimental dot accessor (set) for type JsAssoc.
+  ## Sets the value of a property of name `field` in a JsObject `x` to `value`.
+  var importString: string
+  if validJsName($field):
+    importString = "#." & $field & " = #"
+  else:
+    if not mangledNames.hasKey($field):
+      mangledNames[$field] = mangleJsName($field)
+    importString = "#." & mangledNames[$field] & " = #"
+  let helperName = genSym(nskProc, "helper")
+  result = quote do:
+    proc `helperName`(o: type(`obj`), v: `obj`.V)
+      {.importjs: `importString`.}
+    `helperName`(`obj`, `value`)
+
+macro `.()`*[K: cstring, V: proc](obj: JsAssoc[K, V],
+                                           field: untyped,
+                                           args: varargs[untyped]): auto =
+  ## Experimental "method call" operator for type JsAssoc.
+  ## Takes the name of a method of the JavaScript object (`field`) and calls
+  ## it with `args` as arguments. Here, everything is typechecked, so you do not
+  ## have to worry about `undefined` return values.
+  let dotOp = bindSym"."
+  result = quote do:
+    (`dotOp`(`obj`, `field`))()
+  for elem in args:
+    result.add elem
+
+# Iterators:
+
+iterator pairs*(obj: JsObject): (cstring, JsObject) =
+  ## Yields tuples of type `(cstring, JsObject)`, with the first entry
+  ## being the `name` of a fields in the JsObject and the second being its
+  ## value wrapped into a JsObject.
+  var k: cstring
+  var v: JsObject
+  {.emit: "for (var `k` in `obj`) {".}
+  {.emit: "  if (!`obj`.hasOwnProperty(`k`)) { continue; }".}
+  {.emit: "  `v` = `obj`[`k`];".}
+  yield (k, v)
+  {.emit: "}".}
+
+iterator items*(obj: JsObject): JsObject =
+  ## Yields the `values` of each field in a JsObject, wrapped into a JsObject.
+  var v: JsObject
+  {.emit: "for (var k in `obj`) {".}
+  {.emit: "  if (!`obj`.hasOwnProperty(k)) { continue; }".}
+  {.emit: "  `v` = `obj`[k];".}
+  yield v
+  {.emit: "}".}
+
+iterator keys*(obj: JsObject): cstring =
+  ## Yields the `names` of each field in a JsObject.
+  var k: cstring
+  {.emit: "for (var `k` in `obj`) {".}
+  {.emit: "  if (!`obj`.hasOwnProperty(`k`)) { continue; }".}
+  yield k
+  {.emit: "}".}
+
+iterator pairs*[K: JsKey, V](assoc: JsAssoc[K, V]): (K,V) =
+  ## Yields tuples of type `(K, V)`, with the first entry
+  ## being a `key` in the JsAssoc and the second being its corresponding value.
+  var k: cstring
+  var v: V
+  {.emit: "for (var `k` in `assoc`) {".}
+  {.emit: "  if (!`assoc`.hasOwnProperty(`k`)) { continue; }".}
+  {.emit: "  `v` = `assoc`[`k`];".}
+  yield (k.toJsKey(K), v)
+  {.emit: "}".}
+
+iterator items*[K, V](assoc: JsAssoc[K, V]): V =
+  ## Yields the `values` in a JsAssoc.
+  var v: V
+  {.emit: "for (var k in `assoc`) {".}
+  {.emit: "  if (!`assoc`.hasOwnProperty(k)) { continue; }".}
+  {.emit: "  `v` = `assoc`[k];".}
+  yield v
+  {.emit: "}".}
+
+iterator keys*[K: JsKey, V](assoc: JsAssoc[K, V]): K =
+  ## Yields the `keys` in a JsAssoc.
+  var k: cstring
+  {.emit: "for (var `k` in `assoc`) {".}
+  {.emit: "  if (!`assoc`.hasOwnProperty(`k`)) { continue; }".}
+  yield k.toJsKey(K)
+  {.emit: "}".}
+
+# Literal generation
+
+macro `{}`*(typ: typedesc, xs: varargs[untyped]): auto =
+  ## Takes a `typedesc` as its first argument, and a series of expressions of
+  ## type `key: value`, and returns a value of the specified type with each
+  ## field `key` set to `value`, as specified in the arguments of `{}`.
+  ##
+  ## Example:
+  ##
+  ##   ```nim
+  ##   # Let's say we have a type with a ton of fields, where some fields do not
+  ##   # need to be set, and we do not want those fields to be set to `nil`:
+  ##   type
+  ##     ExtremelyHugeType = ref object
+  ##       a, b, c, d, e, f, g: int
+  ##       h, i, j, k, l: cstring
+  ##       # And even more fields ...
+  ##
+  ##   let obj = ExtremelyHugeType{ a: 1, k: "foo".cstring, d: 42 }
+  ##
+  ##   # This generates roughly the same JavaScript as:
+  ##   {.emit: "var obj = {a: 1, k: "foo", d: 42};".}
+  ##   ```
+  let a = ident"a"
+  var body = quote do:
+    var `a` {.noinit.}: `typ`
+    {.emit: "`a` = {};".}
+  for x in xs.children:
+    if x.kind == nnkExprColonExpr:
+      let
+        k = x[0]
+        kString = quote do:
+          when compiles($`k`): $`k` else: "invalid"
+        v = x[1]
+      body.add quote do:
+        when compiles(`a`.`k`):
+          `a`.`k` = `v`
+        elif compiles(`a`[`k`]):
+          `a`[`k`] = `v`
+        else:
+          `a`[`kString`] = `v`
+
+    else:
+      error("Expression `" & $x.toStrLit & "` not allowed in `{}` macro")
+
+  body.add quote do:
+    return `a`
+
+  result = quote do:
+    proc inner(): `typ` {.gensym.} =
+      `body`
+    inner()
+
+# Macro to build a lambda using JavaScript's `this`
+# from a proc, `this` being the first argument.
+
+proc replaceSyms(n: NimNode): NimNode =
+  if n.kind == nnkSym:
+    result = newIdentNode($n)
+  else:
+    result = n
+    for i in 0..<n.len:
+      result[i] = replaceSyms(n[i])
+
+macro bindMethod*(procedure: typed): auto {.deprecated: "Don't use it with closures".} =
+  ## Takes the name of a procedure and wraps it into a lambda missing the first
+  ## argument, which passes the JavaScript builtin `this` as the first
+  ## argument to the procedure. Returns the resulting lambda.
+  ##
+  ## Example:
+  ##
+  ## We want to generate roughly this JavaScript:
+  ##   ```js
+  ##   var obj = {a: 10};
+  ##   obj.someMethod = function() {
+  ##     return this.a + 42;
+  ##   };
+  ##   ```
+  ##
+  ## We can achieve this using the `bindMethod` macro:
+  ##
+  ##   ```nim
+  ##   let obj = JsObject{ a: 10 }
+  ##   proc someMethodImpl(that: JsObject): int =
+  ##     that.a.to(int) + 42
+  ##   obj.someMethod = bindMethod someMethodImpl
+  ##
+  ##   # Alternatively:
+  ##   obj.someMethod = bindMethod
+  ##     proc(that: JsObject): int = that.a.to(int) + 42
+  ##   ```
+  if not (procedure.kind == nnkSym or procedure.kind == nnkLambda):
+    error("Argument has to be a proc or a symbol corresponding to a proc.")
+  var
+    rawProc = if procedure.kind == nnkSym:
+        getImpl(procedure)
+      else:
+        procedure
+    args = rawProc[3].copyNimTree.replaceSyms
+    thisType = args[1][1]
+    params = newNimNode(nnkFormalParams).add(args[0])
+    body = newNimNode(nnkLambda)
+    this = newIdentNode("this")
+    # construct the `this` parameter:
+    thisQuote = quote do:
+      var `this` {.nodecl, importc: "this".}: `thisType`
+    call = newNimNode(nnkCall).add(rawProc[0], thisQuote[0][0][0])
+  # construct the procedure call inside the method
+  if args.len > 2:
+    for idx in 2..args.len-1:
+      params.add(args[idx])
+      call.add(args[idx][0])
+  body.add(newNimNode(nnkEmpty),
+      rawProc[1],
+      rawProc[2],
+      params,
+      rawProc[4],
+      rawProc[5],
+      newTree(nnkStmtList, thisQuote, call)
+  )
+  result = body