diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index da89ee31..fcf52092 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -13,4 +13,5 @@ jobs:
with:
nim-version: '1.6.x'
- run: nimble -y install
+ - run: nimble docsdeps
- run: nimble test
diff --git a/docsrc/caesar.nim b/docsrc/caesar.nim
new file mode 100644
index 00000000..f824ef10
--- /dev/null
+++ b/docsrc/caesar.nim
@@ -0,0 +1,61 @@
+import nimib
+
+nbInit
+
+
+nbKaraxCode:
+ import std / [strutils, math]
+
+ proc encryptChar(c: char, shift: int): char =
+ let c_normalized = c.ord - 'a'.ord # a is 0, z is 25
+ var c_encrypted = euclMod(c_normalized + shift, 26) + 'a'.ord
+ result = c_encrypted.char
+
+ proc encryptString(s: string, shift: int): string =
+ for c in s:
+ if c in 'a' .. 'z':
+ result.add encryptChar(c, shift)
+ else:
+ result.add c
+
+ var cipherText, plainText: string
+ let ciphertextId = "ciphertext"
+ let plaintextId = "plaintext"
+ let shiftSliderId = "shiftSlider"
+ let encodeButtonId = "encodeButton"
+ let decodeButtonId = "decodeButton"
+ var shiftString = "3"
+ karaxHtml:
+ label:
+ text "Plaintext"
+ textarea(id = plaintextId, placeholder = "You can encrypt this message or you can try to decrypt the message below...")
+ hr()
+ label:
+ text "Ciphertext"
+ textarea(id = ciphertextId):
+ text "oek vekdt jxu iushuj auo! weet meha! dem oek sqd uqj q squiqh iqbqt qi q fhypu, okcco!"
+ hr()
+ label:
+ text "Shift/Key: " & shiftString
+ input(`type` = "range", min = "0", max = "25", value = "3", id = shiftSliderId):
+ proc oninput() =
+ let slider = getVNodeById(shiftSliderId)
+ shiftString = $slider.getInputText
+ button(id = encodeButtonId):
+ text "Encrypt"
+ proc onClick() =
+ let shift = ($getVNodeById(shiftSliderId).getInputText).parseInt
+ let plaintext = ($getVNodeById(plaintextId).getInputText).toLower
+ let ciphertext = encryptString(plaintext, shift)
+ getVNodeById(ciphertextId).setInputText ciphertext
+ button(id = decodeButtonId):
+ text "Decrypt"
+ proc onClick() =
+ let shift = ($getVNodeById(shiftSliderId).getInputText).parseInt
+ let ciphertext = ($getVNodeById(ciphertextId).getInputText).toLower
+ let plaintext = encryptString(ciphertext, -shift) # encrypt with -shift to decrypt
+ getVNodeById(plaintextId).setInputText plainText
+
+
+
+nbSave
\ No newline at end of file
diff --git a/docsrc/counters.nim b/docsrc/counters.nim
new file mode 100644
index 00000000..c3a74db7
--- /dev/null
+++ b/docsrc/counters.nim
@@ -0,0 +1,125 @@
+import std / [strutils]
+import nimib
+
+nbInit
+
+nbText: hlMd"""
+# Counters - Creating reusable widgets
+
+This document will show you how to create reusable widgets using `nbCodeToJs`. Specifically we will make a counter:
+A button which increases a counter each time you click it. We will do this in two different ways, using `std/dom` and `karax`.
+## std/dom
+
+The first method is to use Nim like you would have used Javascript using `getElementById` and `addEventListener`:
+"""
+nbCode:
+ ## 1:
+ template counterButton(id: string) =
+ let labelId = "label-" & id
+ let buttonId = "button-" & id
+ ## 2:
+ nbRawOutput: """
+0
+Click me
+""" % [labelId, buttonId]
+ ## 3:
+ nbCodeToJs(labelId, buttonId):
+ import std/dom
+ ## 4:
+ var label = getElementById(labelId.cstring)
+ var button = getElementById(buttonId.cstring)
+ ## 5:
+ var counter: int = 0
+ button.addEventListener("click",
+ proc (ev: Event) =
+ counter += 1
+ label.innerHtml = ($counter).cstring
+ )
+
+nbText: hlMd"""
+Let's explain each part of the code:
+1. We define a template called `counterButton` which will create a new counter button. So if you call it somewhere it will
+place the widget there, that's the reusable part done. But it also takes an input `id: string`. This is to solve the problem of each widget needing unique ids. It can also be done with `nb.newId` as will be used in the Karax example.
+2. Here we emit the `` and `` tags and insert their ids.
+3. `nbCodeToJs` is the template that will turn our Nim code into Javascript and we are capturing `labelId` and `buttonId` (Important that you capture all used variables defined outside the code block). `std/dom` is where many dom-manipulation functions are located.
+4. We fetch the elements we emitted above by their ids. Remember that most javascript functions want `cstring`s!
+5. We create a variable `counter` to keep track of the counter and add the eventlistener to the `button` element. There we increase the counter and update the `innerHtml` of the `label`.
+
+Here we have the button in action: `counterButton("uniqueString")`
+"""
+
+counterButton("uniqueString")
+
+nbText: """And here is another independent counter: `counterButton("anotherUniqueString")`"""
+
+counterButton("anotherUniqueString")
+
+nbText: hlMd"""
+## Karax
+
+The second method uses Karax to construct the HTML and attach an eventlistener to the button:
+"""
+
+nbCode:
+ template karaxButton() =
+ ## 1:
+ nbKaraxCode:
+ ## 2:
+ var counter = 0
+ ## 3:
+ karaxHtml:
+ label:
+ text "Counter: " & $counter
+ button:
+ text "Click me (karax)"
+ ## 4:
+ proc onClick() =
+ counter += 1
+
+
+nbText: hlMd"""
+Here's what each part of the code does:
+1. `nbKaraxCode` is a convinience template for writing karax components. Karax is automatically imported inside the code block.
+2. Setup `counter` to keep track of the count.
+3. `karaxHtml` is a convinience wrapper for the karax dsl. The code block will automatically be inserted inside a `buildHtml(tdiv)`.
+4. The eventlistener is inlined in the `button:`.
+
+Here is the button in action: `karaxButton()`
+"""
+
+karaxButton()
+
+nbText: """And here is another independent counter: `karaxButton()`"""
+
+karaxButton()
+
+nbText: hlMd"""
+## Exercise
+
+Modify the counter templates to include a reset button which sets the counter to 0 again like this:
+"""
+
+## Karax with reset button
+template karaxButtonWithReset() =
+ nbKaraxCode:
+ var counter = 0
+ karaxHtml:
+ label:
+ text "Counter: " & $counter
+ button:
+ text "Click me (karax)"
+ proc onClick() =
+ counter += 1
+ button:
+ text "Reset"
+ proc onClick() =
+ counter = 0
+
+karaxButtonWithReset()
+
+nbText: hlMd"""
+If you click "Show source" at the bottom of the page you can find this implemented in Karax in template `karaxButtonWithReset`.
+"""
+
+
+nbSave
\ No newline at end of file
diff --git a/docsrc/index.nim b/docsrc/index.nim
index 62b2a4d1..6112497b 100644
--- a/docsrc/index.nim
+++ b/docsrc/index.nim
@@ -5,7 +5,7 @@ nb.title = "Nimib Docs"
let
repo = "https://github.com/pietroppeter/nimib"
- docs = "https://pietroppeter.github.io/nimib"
+ docs = if defined(useMdBackend): "https://pietroppeter.github.io/nimib" else: "."
hello = read(nb.srcDir / "hello.nim".RelativeFile)
assets = "docs/static"
highlight = "highlight.nim.js"
@@ -83,6 +83,10 @@ in this repo:
* [numerical]({docs}/numerical.html): example usage of NumericalNim (example of custom style, usage of latex)
* [cheatsheet]({docs}/cheatsheet.html): markdown cheatsheet (example of a custom block, custom highlighting and a simple TOC)
* [mostaccio]({docs}/mostaccio.html): examples of usage of nim-mustache and of dark mode.
+* [interactivity]({docs}/interactivity.html): shows the basic API of creating interactive elements using `nbCodeToJs`.
+* [counter]({docs}/counters.html): shows how to create reusable interactive widgets by creating a counter button.
+* [caesar]({docs}/caesar.html): a Caesar cipher implemented using `nbCodeToJs` and `karax`.
+
elsewhere:
diff --git a/docsrc/interactivity.nim b/docsrc/interactivity.nim
new file mode 100644
index 00000000..290c7fa6
--- /dev/null
+++ b/docsrc/interactivity.nim
@@ -0,0 +1,93 @@
+import nimib
+
+nbInit
+
+nbText: hlMd"""
+# Creating interactive components in Nimib
+
+Nimib can easily be used to create static content with `nbText` and `nbCode`, but did you know that you can create interactive
+content as well? And that you can do it all in Nim even! This can be achieved using either the `nbCodeToJs`-API or `nbKaraxCode`.
+They work by compiling Nim code into javascript and adding it to the resulting HTML file.
+This means that arbitrary Javascript can be written but also that Karax, which compiles to javascript, also can be used.
+
+## nbCodeToJsInit
+This is the fundamental API used for compiling Nim-snippets to javascript. It consists of three templates:
+- `nbCodeToJsInit` - Creates a new code script that further code can be added to later.
+- `addCodeToJs` - Adds to an existing code script
+- `addToDocAsJs` - Takes the Nim code in a script and compiles it to javascript.
+Here is a basic example:
+"""
+
+nbCode:
+ let script = nbCodeToJsInit:
+ echo "Hello world!"
+ let x = 3.14
+ script.addCodeToJs(x):
+ echo "Pi is roughly ", x
+ ## Uncomment this line:
+ ##script.addToDocAsJs()
+script.addToDocAsJs()
+nbCodeToJsShowSource()
+
+
+nbText: hlMd"""
+The reason `script.addToDocAsJs()` is commented out is just a limitation of nimib not handling nested blocks well.
+If you now go to your browser's javascript console you should see `Hello world` and `Pi is roughly 3.14` printed there.
+What is up with `script.addCodeToJs(x)` though? Why is `(x)` needed? It is because we have to capture the value of `x`
+to be able to use it in the javascript. The code block will basically be copy-pasted into a separate file and
+compiled into javascript. And `x` isn't defined there so we have to capture it. This is true for any variable that
+we want to use that is defined outside the script blocks.
+
+## nbCodeToJs
+This is basically a shorthand for running `nbCodeToJsInit` and `addToDocAsJs` in a single call:
+```nim
+let x = 3.14
+nbJsCode(x):
+ echo "Pi is roughly ", x
+```
+
+## nbKaraxCode
+
+If you want to write a component using karax this is the template for you!
+A normal karax program has the following structure:
+```nim
+nbCodeToJs(rootId):
+ import karax / [kbase, karax, karaxdsl, vdom, compact, jstrutils, kdom]
+
+ karaxCode # some code, set up global variables for example
+
+ proc createDom(): VNode =
+ result = buildHtml(tdiv):
+ karaxHtmlCode # html karax code
+
+ setRenderer(createDom, root=rootId.cstring)
+```
+where `karaxCode` and `karaxHtmlCode` can be arbitrary code. Using `nbKaraxCode` it can instead be written as:
+```nim
+nbKaraxCode:
+ karaxCode
+ karaxHtml:
+ karaxHtmlCode
+```
+This reduces the boilerplate and makes for more readable code! Karax is automatically imported for you (the modules in `karax / prelude`) and `karaxHtml`
+is a template that writes `createDom` and `setRenderer` for you so you only have to provide the body of the `buildHtml` call. Here's a basic example:
+"""
+
+nbCode:
+ template karaxExample =
+ let x = 3.14
+ nbKaraxCode(x):
+ var message = "Pi is roughly " & $x
+ karaxHtml:
+ p:
+ text message
+ button:
+ text "Click me!"
+ proc onClick() =
+ message = "Poof! Gone!"
+
+nbText: "This is the output this code produces:"
+
+karaxExample()
+
+nbSave
\ No newline at end of file
diff --git a/nimib.nimble b/nimib.nimble
index 9110fce6..c1e64e94 100644
--- a/nimib.nimble
+++ b/nimib.nimble
@@ -15,7 +15,7 @@ requires "mustache >= 0.2.1"
requires "toml_serialization >= 0.2.0"
task docsdeps, "install dependendencies required for doc building":
- exec "nimble -y install ggplotnim@0.4.9 numericalnim@0.6.1 nimoji nimpy"
+ exec "nimble -y install ggplotnim@0.4.9 numericalnim@0.6.1 nimoji nimpy karax@#head"
task test, "General tests":
exec "nim r -d:nimibPreviewCodeAsInSource tests/tsources.nim"
@@ -38,6 +38,9 @@ task docs, "Build documentation":
exec "nim r docsrc/cheatsheet.nim"
exec "nim r docsrc/files.nim"
exec "nim r docsrc/index.nim"
+ exec "nim r docsrc/interactivity.nim"
+ exec "nim r docsrc/counters.nim"
+ exec "nim r docsrc/caesar.nim"
when not defined(nimibDocsSkipPenguins):
exec "nim r docsrc/penguins.nim"
exec "nimble readme"
diff --git a/src/nimib.nim b/src/nimib.nim
index fde5f0f7..207d9626 100644
--- a/src/nimib.nim
+++ b/src/nimib.nim
@@ -1,6 +1,7 @@
-import std/[os, strutils, sugar]
-import nimib / [types, blocks, docs, boost, config, options, capture]
-export types, blocks, docs, boost, sugar
+import std/[os, strutils, sugar, strformat, macros, macrocache, sequtils, jsonutils, random]
+export jsonutils
+import nimib / [types, blocks, docs, boost, config, options, capture, jsutils]
+export types, blocks, docs, boost, sugar, jsutils
# types exports mustache, tables, paths
from nimib / themes import nil
@@ -16,7 +17,6 @@ template moduleAvailable*(module: untyped): bool =
template nbInit*(theme = themes.useDefault, backend = renders.useHtmlBackend, thisFileRel = "") =
var nb {.inject.}: NbDoc
-
nb.initDir = getCurrentDir().AbsoluteDir
loadOptions nb
loadCfg nb
@@ -124,6 +124,38 @@ template nbRawOutput*(content: string) =
newNbSlimBlock("nbRawOutput"):
nb.blk.output = content
+
+template nbCodeToJsInit*(args: varargs[untyped]): NbBlock =
+ let (code, originalCode) = nimToJsString(true, args)
+ var result = NbBlock(command: "nbCodeToJs", code: originalCode, context: newContext(searchDirs = @[], partials = nb.partials), output: "")
+ result.context["transformedCode"] = code
+ result
+
+template addCodeToJs*(script: NbBlock, args: varargs[untyped]) =
+ let (code, originalCode) = nimToJsString(false, args)
+ script.code &= "\n" & originalCode
+ script.context["transformedCode"] = script.context["transformedCode"].vString & "\n" & code
+
+
+template addToDocAsJs*(script: NbBlock) =
+ nb.blocks.add script
+ nb.blk = script
+
+template nbCodeToJs*(args: varargs[untyped]) =
+ let script = nbCodeToJsInit(args)
+ script.addToDocAsJs
+
+
+when moduleAvailable(karax/kbase):
+ template nbKaraxCode*(args: varargs[untyped]) =
+ let rootId = "karax-" & $nb.newId()
+ nbRawOutput: "
"
+ nbKaraxCodeBackend(rootId, args)
+
+template nbCodeToJsShowSource* =
+ nb.blk.context["js_show_nim_source"] = true
+
+
template nbClearOutput*() =
if not nb.blk.isNil:
nb.blk.output = ""
diff --git a/src/nimib/boost.nim b/src/nimib/boost.nim
index 7f60e2bb..068dcb59 100644
--- a/src/nimib/boost.nim
+++ b/src/nimib/boost.nim
@@ -7,7 +7,7 @@ template md*(s: string): string {.deprecated: "use hlMd instead".} =
template fmd*(s: string): string {.deprecated: "use hlMdF instead".} =
## Template for use with NimiBoost to mark strings to be syntax-highlighted as Markdown.
- ## Equivilent to ``&s`` so it applies string interpolation.
+ ## Equivalent to ``&s`` so it applies string interpolation.
&s
template hlMd*(s: string): string =
@@ -17,7 +17,7 @@ template hlMd*(s: string): string =
template hlMdF*(s: string): string =
## Template for use with NimiBoost to mark strings to be syntax-highlighted as Markdown.
- ## Equivilent to ``&s`` so it applies string interpolation.
+ ## Equivalent to ``&s`` so it applies string interpolation.
&s
template hlPy*(s: string): string =
@@ -27,5 +27,15 @@ template hlPy*(s: string): string =
template hlPyF*(s: string): string =
## Template for use with NimiBoost to mark strings to be syntax-highlighted as Python.
- ## Equivilent to ``&s`` so it applies string interpolation.
+ ## Equivalent to ``&s`` so it applies string interpolation.
+ &s
+
+template hlNim*(s: string): string =
+ ## Template for use with NimiBoost to mark strings to be syntax-highlighted as Nim.
+ ## It is a no-op and returns the orginal string.
+ s
+
+template hlNimF*(s: string): string =
+ ## Template for use with NimiBoost to mark strings to be syntax-highlighted as Nim.
+ ## Equivalent to ``&s`` so it applies string interpolation.
&s
\ No newline at end of file
diff --git a/src/nimib/jsutils.nim b/src/nimib/jsutils.nim
new file mode 100644
index 00000000..2c6a2094
--- /dev/null
+++ b/src/nimib/jsutils.nim
@@ -0,0 +1,226 @@
+import std / [macros, macrocache, tables, strutils, strformat, sequtils, sugar]
+
+
+proc contains(tab: CacheTable, keyToCheck: string): bool =
+ for key, val in tab:
+ if key == keyToCheck:
+ return true
+ return false
+
+const validCodeTable = CacheTable"validCodeTable"
+const invalidCodeTable = CacheTable"invalidCodeTable"
+var tabMapIdents {.compiletime.}: Table[string, NimNode]
+
+macro typedChecker(n: typed): untyped = discard
+macro checkIsValidCode(n: untyped): untyped =
+ result = quote do:
+ when compiles(typedChecker(`n`)):
+ true
+ else:
+ false
+
+macro addValid(key: string, s: typed): untyped =
+ # If it is valid we want it typed
+ if key.strVal notin validCodeTable:
+ validCodeTable[key.strVal] = s
+
+macro addInvalid(key: string, s: untyped): untyped =
+ # If it is invalid we want it untyped
+ if key.strVal notin invalidCodeTable:
+ invalidCodeTable[key.strVal] = s
+
+proc gensymProcIterConverter(n: NimNode, replaceProcs: bool) =
+ ## By default procs, iterators and converters are injected and will share the same name in the resulting javascript.
+ ## Therefore we gensym them here to give them unique names. Also replace the references to it.
+ ## replaceProcs: whether to replace procs names or not. It will replace existing names regardless.
+ for i in 0 ..< n.len:
+ case n[i].kind
+ of nnkProcDef, nnkIteratorDef, nnkConverterDef:
+ if replaceProcs:
+ if n[i][0].kind == nnkPostfix: # foo*
+ let oldIdent = n[i][0][1].strVal.nimIdentNormalize
+ let newIdent = gensym(ident=oldIdent).repr.ident
+ n[i][0][1] = newIdent
+ tabMapIdents[oldIdent] = newIdent
+ else:
+ let oldIdent = n[i][0].strVal.nimIdentNormalize
+ let newIdent = gensym(ident=oldIdent).repr.ident
+ n[i][0] = newIdent
+ tabMapIdents[oldIdent] = newIdent
+ # Function might be recursive or contain other procs, loop through it's body as well
+ for child in n[i][6]:
+ gensymProcIterConverter(child, replaceProcs)
+ of nnkLambda:
+ # rewrite from:
+ # proc () = discard
+ # to
+ # block:
+ # proc lambda_gensym() = discard
+ # lambda_gensym
+ let p = nnkProcDef.newTree()
+ n[i].copyChildrenTo p
+ let newIdent = gensym(ident="lambda")
+ p[0] = newIdent
+ # loop through proc body as well
+ for child in p[6]:
+ gensymProcIterConverter(child, replaceProcs)
+ n[i] = newStmtList(p, newIdent)
+ of nnkSym, nnkIdent:
+ let oldIdent = n[i].strVal.nimIdentNormalize
+ if oldIdent in tabMapIdents:
+ n[i] = tabMapIdents[oldIdent]
+ of nnkCall:
+ # Check if it is karaxHtml:
+ # if so set replaceProcs = false for the children
+ if n[i][0].eqIdent("karaxHtml"):
+ gensymProcIterConverter(n[i][1], false)
+ else:
+ gensymProcIterConverter(n[i], replaceProcs)
+ else:
+ gensymProcIterConverter(n[i], replaceProcs)
+
+proc degensymAst(n: NimNode, removeGensym = false) =
+ for i in 0 ..< n.len:
+ case n[i].kind
+ of nnkIdent, nnkSym:
+ let str = n[i].strVal
+ if "`gensym" in str:
+ let newStr = str.split("`gensym")[0].nimIdentNormalize
+ var newSym: NimNode
+ if removeGensym: # remove gensym all together, useful for removing gensym noise when showing code
+ newSym = ident(newStr)
+ else: # replace gensym with one that is accepted by the parser
+ # If this symbol is already used in this script, use the gensym'd symbol from tabMapIdents
+ if newStr in tabMapIdents:
+ newSym = tabMapIdents[newStr]
+ else: # else create a gensym'd symbol and add it to the table
+ newSym = gensym(ident=newStr).repr.ident
+ tabMapIdents[newStr] = newSym
+ n[i] = newSym
+ echo "Swapped ", str, " for ", newSym.repr
+ else:
+ degensymAst(n[i], removeGenSym)
+
+proc genCapturedAssignment(capturedVariables, capturedTypes: seq[NimNode]): tuple[code: NimNode, placeholders: seq[NimNode]] =
+ result.code = newStmtList()
+ # generate fromJSON loading and then add entire body afterwards
+ if capturedVariables.len > 0:
+ result.code.add quote do:
+ import std/json
+ for (cap, capType) in zip(capturedVariables, capturedTypes):
+ let placeholder = gensym(ident="placeholder")
+ result.placeholders.add placeholder
+ result.code.add quote do:
+ let `cap` = parseJson(`placeholder`).to(`capType`)
+
+macro nimToJsStringSecondStage*(key: static string, captureVars: varargs[typed]): untyped =
+ let captureVars = toSeq(captureVars)
+
+ let captureTypes = collect:
+ for cap in captureVars:
+ cap.getTypeInst
+
+ # dispatch either to string based if the body has type string
+ # or to typed version otherwise.
+ var body: NimNode
+ if key in validCodeTable: # type information is available in this branch
+ body = validCodeTable[key]
+ if captureVars.len == 0 and body.getType.typeKind == ntyString:
+ # It is a string, return it as is is.
+ result = nnkTupleConstr.newTree(body, body) #body # return tuple of (body, body)
+ return
+ elif captureVars.len > 0 and body.getType.typeKind == ntyString:
+ error("When passing in a string capturing variables is not supported!", body)
+ #elif body.getType.typeKind != ntyVoid:
+ # error("Script expression must be discarded", body)
+ else:
+ # It is not a string, get the untyped body instead then
+ body = invalidCodeTable[key]
+ elif key in invalidCodeTable:
+ body = invalidCodeTable[key]
+ else:
+ error(&"Nimib error: key {key} not in any of the tables. Please open an issue on Github with a minimal reproducible example")
+ # Now we have the body!
+ body = body.copyNimTree()
+ # 1. Generate the captured variable assignments and return placeholders
+ let (capAssignments, placeholders) = genCapturedAssignment(captureVars, captureTypes)
+ # 2. Stringify code
+ let code = newStmtList(capAssignments, body).copyNimTree()
+ code.gensymProcIterConverter(replaceProcs=true)
+ code.degensymAst()
+ var codeText = code.toStrLit
+ # 3. Generate code which does the serialization and replacement of placeholders
+ let codeTextIdent = genSym(NimSymKind.nskVar ,ident="codeText")
+ result = newStmtList()
+ result.add newVarStmt(codeTextIdent, codeText)
+ for i in 0 .. captureVars.high:
+ let placeholder = placeholders[i].repr.newLit
+ let varIdent = captureVars[i]
+ let serializedValue = quote do:
+ $(toJson(`varIdent`))
+ result.add quote do:
+ `codeTextIdent` = `codeTextIdent`.replace(`placeholder`, "\"\"\"" & `serializedValue` & "\"\"\"")
+ # return tuple of the transformed code to be compiled and the prettified code for visualization
+ body.degensymAst(removeGenSym=true) # remove `gensym if code was written in a template
+ result.add nnkTupleConstr.newTree(codeTextIdent, body.toStrLit)
+
+macro nimToJsString*(isNewScript: static bool, args: varargs[untyped]): untyped =
+ if args.len == 0:
+ error("nbCodeToJs needs a code block to be passed", args)
+
+ # If new script, clear the table.
+ if isNewScript:
+ tabMapIdents.clear()
+
+ let body = args[^1]
+ let captureVars =
+ if args.len == 1:
+ @[]
+ else:
+ args[0 ..< ^1]
+
+ # Save UNTYPED body for access later.
+ # It's important that it is untyped to be able to combine
+ # multiple code snippets.
+ let key = body.repr
+
+ result = newStmtList()
+ result.add quote do:
+ when checkIsValidCode(`body`):
+ addValid(`key`, `body`)
+ addInvalid(`key`, `body`) # Add this here as we want to keep the untyped body as well
+ else:
+ addInvalid(`key`, `body`)
+ var nextArgs = @[newLit(key)]
+ nextArgs.add captureVars
+ result.add newCall("nimToJsStringSecondStage", nextArgs)
+
+macro nbKaraxCodeBackend*(rootId: untyped, args: varargs[untyped]) =
+ if args.len == 0:
+ error("nbKaraxCode needs a code block to be passed", args)
+
+ let body = args[^1]
+ let captureVars =
+ if args.len == 1:
+ @[]
+ else:
+ args[0 ..< ^1]
+
+ let newBody = quote do:
+ import karax / [kbase, karax, karaxdsl, vdom, compact, jstrutils, kdom]
+
+ template karaxHtml(body: untyped) =
+ proc createDom(): VNode =
+ result = buildHtml(tdiv):
+ body # html karax code
+ setRenderer(createDom, root=`rootId`.cstring)
+
+ `body`
+
+ var callArgs = @[rootId]
+ callArgs.add captureVars
+ callArgs.add newBody
+
+ let call = newCall(ident"nbCodeToJs", callArgs)
+
+ result = call
\ No newline at end of file
diff --git a/src/nimib/renders.nim b/src/nimib/renders.nim
index dd59dc31..fbaa5c19 100644
--- a/src/nimib/renders.nim
+++ b/src/nimib/renders.nim
@@ -1,6 +1,6 @@
-import types, strutils, markdown, mustache, sugar
+import std / [strutils, tables, sugar, os, strformat, random, sequtils]
+import types, markdown, mustache
export escapeTag # where is this used? why do I export? a better solution is to use xmlEncode
-import tables
import highlight
import mustachepkg/values
@@ -10,6 +10,23 @@ proc mdOutputToHtml(doc: var NbDoc, blk: var NbBlock) =
proc highlightCode(doc: var NbDoc, blk: var NbBlock) =
blk.context["codeHighlighted"] = highlightNim(blk.code)
+proc compileNimToJs(doc: var NbDoc, blk: var NbBlock) =
+ let tempdir = getTempDir() / "nimib"
+ createDir(tempdir)
+ let (dir, filename, ext) = doc.thisFile.splitFile()
+ let nimfile = dir / (filename & "_nbCodeToJs_" & $doc.newId() & ext).RelativeFile
+ let jsfile = tempdir / "out.js"
+ writeFile(nimfile, blk.context["transformedCode"].vString)
+ let kxiname = "nimib_kxi_" & $doc.newId()
+ let errorCode = execShellCmd(&"nim js -d:danger -d:kxiname=\"{kxiname}\" -o:{jsfile} {nimfile}")
+ if errorCode != 0:
+ raise newException(OSError, "The compilation of a javascript file failed! Did you remember to capture all needed variables?\n" & $nimfile)
+ removeFile(nimfile)
+ let jscode = readFile(jsfile)
+ removeFile(jsfile)
+ blk.output = jscode
+ blk.context["output"] = jscode
+
proc useHtmlBackend*(doc: var NbDoc) =
doc.partials["nbText"] = "{{&outputToHtml}}"
doc.partials["nbCode"] = """
@@ -31,14 +48,22 @@ proc useHtmlBackend*(doc: var NbDoc) =
{{content}}
"""
+ doc.partials["nbCodeToJs"] = """
+{{>nbJsScriptNimSource}}
+{{>nbJsScript}}"""
+ doc.partials["nbJsScriptNimSource"] = "{{#js_show_nim_source}}{{>nbCodeSource}}{{/js_show_nim_source}}"
+ doc.partials["nbJsScript"] = ""
+
# I prefer to initialize here instead of in nimib (each backend should re-initialize)
doc.renderPlans = initTable[string, seq[string]]()
doc.renderPlans["nbText"] = @["mdOutputToHtml"]
doc.renderPlans["nbCode"] = @["highlightCode"] # default partial automatically escapes output (code is escaped when highlighting)
+ doc.renderPlans["nbCodeToJs"] = @["compileNimToJs", "highlightCode"]
doc.renderProcs = initTable[string, NbRenderProc]()
doc.renderProcs["mdOutputToHtml"] = mdOutputToHtml
doc.renderProcs["highlightCode"] = highlightCode
+ doc.renderProcs["compileNimToJs"] = compileNimToJs
proc useMdBackend*(doc: var NbDoc) =
doc.partials["document"] = """
diff --git a/src/nimib/types.nim b/src/nimib/types.nim
index 52b0e634..1d1a482e 100644
--- a/src/nimib/types.nim
+++ b/src/nimib/types.nim
@@ -1,6 +1,6 @@
import mustache, std / tables, nimib / paths, std / parseopt
export mustache, tables, paths
-import std / os
+import std / [os]
type
NbBlock* = ref object
@@ -32,6 +32,7 @@ type
templateDirs*: seq[string]
renderPlans*: Table[string, seq[string]]
renderProcs*: Table[string, NbRenderProc]
+ id: int
proc thisDir*(doc: NbDoc): AbsoluteDir = doc.thisFile.splitFile.dir
proc srcDir*(doc: NbDoc): AbsoluteDir =
@@ -45,4 +46,8 @@ proc homeDir*(doc: NbDoc): AbsoluteDir =
else:
doc.cfgDir / doc.cfg.homeDir.RelativeDir
proc thisFileRel*(doc: NbDoc): RelativeFile = doc.thisFile.relativeTo doc.srcDir
-proc srcDirRel*(doc: NbDoc): RelativeDir = doc.srcDir.relativeTo doc.thisDir
\ No newline at end of file
+proc srcDirRel*(doc: NbDoc): RelativeDir = doc.srcDir.relativeTo doc.thisDir
+proc newId*(doc: var NbDoc): int =
+ ## Provides a unique integer each time it is called
+ result = doc.id
+ inc doc.id
\ No newline at end of file
diff --git a/tests/tnimib.nim b/tests/tnimib.nim
index 2d667e11..c4c9a8df 100644
--- a/tests/tnimib.nim
+++ b/tests/tnimib.nim
@@ -1,5 +1,5 @@
-import nimib, strformat
-import unittest
+import std / [unittest, strformat, strutils]
+import nimib
nbInit # todo: add a test suite for nbInit
@@ -128,3 +128,48 @@ print(a)
check nb.blk.code == pyString
check nb.blk.output == "[0, 2, 4]\n3.14\n"
+when moduleAvailable(karax/kbase):
+ suite "nbCodeToJs":
+ test "nbCodeToJs - string":
+ nbCodeToJs: hlNim"""
+ let a = 1
+ echo a
+ """
+ check nb.blk.code == """
+ let a = 1
+ echo a
+ """
+ check nb.blk.context["transformedCode"].vString.len > 0
+
+ test "nbCodeToJs - untyped":
+ nbCodeToJs:
+ let a = 1
+ echo a
+ check nb.blk.code.len > 0
+ check nb.blk.context["transformedCode"].vString.len > 0
+
+ test "nbCodeToJs - untyped, capture variable":
+ let a = 1
+ nbCodeToJs(a):
+ echo a
+ check nb.blk.code.len > 0
+ check nb.blk.context["transformedCode"].vString.len > 0
+
+ test "nbCodeToJsInit + addCodeToJs":
+ let script = nbCodeToJsInit:
+ let a = 1
+ script.addCodeToJs:
+ echo a
+ script.addToDocAsJs
+ check nb.blk.code.len > 0
+ check nb.blk.context["transformedCode"].vString.len > 0
+
+ test "nbKaraxCode":
+ let x = 3.14
+ nbKaraxCode(x):
+ var message = "Pi is roughly " & $x
+ karaxHtml:
+ p:
+ text message
+ check nb.blk.code.len > 0
+ check nb.blk.context["transformedCode"].vString.len > 0