123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521 |
- #
- #
- # 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
- result = quote do:
- proc helper(o: JsObject): JsObject
- {.importjs: `importString`, gensym.}
- helper(`obj`)
- else:
- if not mangledNames.hasKey($field):
- mangledNames[$field] = mangleJsName($field)
- let importString = "#." & mangledNames[$field]
- result = quote do:
- proc helper(o: JsObject): JsObject
- {.importjs: `importString`, gensym.}
- helper(`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 & " = #"
- result = quote do:
- proc helper(o: JsObject, v: auto)
- {.importjs: `importString`, gensym.}
- helper(`obj`, `value`)
- else:
- if not mangledNames.hasKey($field):
- mangledNames[$field] = mangleJsName($field)
- let importString = "#." & mangledNames[$field] & " = #"
- result = quote do:
- proc helper(o: JsObject, v: auto)
- {.importjs: `importString`, gensym.}
- helper(`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] & "(@)"
- result = quote:
- proc helper(o: JsObject): JsObject
- {.importjs: `importString`, gensym, discardable.}
- helper(`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]
- result = quote do:
- proc helper(o: type(`obj`)): `obj`.V
- {.importjs: `importString`, gensym.}
- helper(`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] & " = #"
- result = quote do:
- proc helper(o: type(`obj`), v: `obj`.V)
- {.importjs: `importString`, gensym.}
- helper(`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 =
- ## 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
|