Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compiling nim to javascript #88

Merged
merged 54 commits into from
Jul 2, 2022
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
e6d3b89
add hlNim to boost
HugoGranstrom Jun 15, 2022
ffd6f19
draft for javascript compiling
HugoGranstrom Jun 15, 2022
2cca3ad
progress is made on untyped js
HugoGranstrom Jun 16, 2022
77bf22f
it kinda works!
HugoGranstrom Jun 16, 2022
7ae774e
fix so it works in templates by degensym:ing identifiers
HugoGranstrom Jun 16, 2022
e672ff9
pass -d:kxiname to give each script their own karax instance
HugoGranstrom Jun 27, 2022
c833baa
make sure each body has a unique key
HugoGranstrom Jun 27, 2022
4b02ae4
make sure each script has unique variable names
HugoGranstrom Jun 27, 2022
6020a49
Merge branch 'main' of https://github.com/pietroppeter/nimib into main
HugoGranstrom Jun 28, 2022
879558b
always use the untyped body
HugoGranstrom Jun 28, 2022
3ab4d1d
start writing docs for interactivity
HugoGranstrom Jun 28, 2022
8c46dac
add interactivity to nimble docs
HugoGranstrom Jun 28, 2022
c13a7a5
remove randomCounter
HugoGranstrom Jun 28, 2022
4e8f13c
add nimib random number generator
HugoGranstrom Jun 28, 2022
7bfa74b
move nimToJsString to nimib/jsutils.nim
HugoGranstrom Jun 28, 2022
1777bb5
rename nbCodeToJs API
HugoGranstrom Jun 28, 2022
7f286c4
move rng to NbDoc object
HugoGranstrom Jun 29, 2022
231091e
update interactivity doc to new API
HugoGranstrom Jun 29, 2022
e8ba83e
remove NbScriptType
HugoGranstrom Jun 29, 2022
9093c1a
implement render plan for nbCodeToJs
HugoGranstrom Jun 29, 2022
e6a35e1
show source in interactivity
HugoGranstrom Jun 29, 2022
a054726
Update src/nimib/jsutils.nim
HugoGranstrom Jun 29, 2022
7dd307a
prettier code from nbCodeToJsShowSource
HugoGranstrom Jun 29, 2022
4c46149
Merge branch 'main' of https://github.com/HugoGranstrom/nimib into main
HugoGranstrom Jun 29, 2022
0a56599
fix compile error...
HugoGranstrom Jun 29, 2022
e7cfb42
create empty counters and caesar pages
HugoGranstrom Jun 29, 2022
a742434
skeleton of counters added
HugoGranstrom Jun 29, 2022
0830b0f
finish counters.nim
HugoGranstrom Jun 29, 2022
84b6eb9
add calling examples to counters
HugoGranstrom Jun 29, 2022
b8b3899
add reset button example
HugoGranstrom Jun 30, 2022
3ea8bbe
typos
HugoGranstrom Jun 30, 2022
e3a3c43
caesar skeleton
HugoGranstrom Jun 30, 2022
cfc027f
key → shift and adjust range and default
HugoGranstrom Jun 30, 2022
851e532
add to index and fix relative links
HugoGranstrom Jun 30, 2022
dd9925c
introduce nbDoc.newId
HugoGranstrom Jun 30, 2022
64d3d1f
clean some comments
HugoGranstrom Jun 30, 2022
ac29b63
implement nbKaraxCode
HugoGranstrom Jun 30, 2022
5cf8d46
rewrite docs to use nbKaraxCode
HugoGranstrom Jun 30, 2022
26188df
use textarea and placeholder in caesar
HugoGranstrom Jul 1, 2022
580029e
add very rudimentary tests for nbCodeToJs
HugoGranstrom Jul 1, 2022
dfc4953
minimal change to rerun CI
HugoGranstrom Jul 1, 2022
54ccf63
remove unneccecary import in caesar
HugoGranstrom Jul 1, 2022
efcbcbf
place code.nim next to original file
HugoGranstrom Jul 2, 2022
4ada60d
guard nbKaraxCode with moduleAvailable
HugoGranstrom Jul 2, 2022
26d7f81
guard karax tests
HugoGranstrom Jul 2, 2022
c35bbf9
gensym procs and lambdas
HugoGranstrom Jul 2, 2022
4b9c8ae
ignore to gensym procs inside karaxHtml
HugoGranstrom Jul 2, 2022
56e5bab
install docdeps for test.yml
HugoGranstrom Jul 2, 2022
d8596c0
same logic for iterators and converters as procs
HugoGranstrom Jul 2, 2022
ead5804
it's probably a good idea to normalize identifiers
HugoGranstrom Jul 2, 2022
8607563
Update docsrc/caesar.nim
HugoGranstrom Jul 2, 2022
8830e5b
remove jsfile
HugoGranstrom Jul 2, 2022
cc0f16c
fix equivAlent typo
HugoGranstrom Jul 2, 2022
04579ae
Merge branch 'main' of https://github.com/HugoGranstrom/nimib into main
HugoGranstrom Jul 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions docsrc/caesar.nim
Original file line number Diff line number Diff line change
@@ -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 iqbbqt qi q fhypu, okcco!"
HugoGranstrom marked this conversation as resolved.
Show resolved Hide resolved
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
125 changes: 125 additions & 0 deletions docsrc/counters.nim
Original file line number Diff line number Diff line change
@@ -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: """
<label id="$1">0</label>
<button id="$2">Click me</button>
""" % [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 `<label>` and `<button>` 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
6 changes: 5 additions & 1 deletion docsrc/index.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:

Expand Down
93 changes: 93 additions & 0 deletions docsrc/interactivity.nim
Original file line number Diff line number Diff line change
@@ -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
5 changes: 4 additions & 1 deletion nimib.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -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 [email protected] [email protected] nimoji nimpy"
exec "nimble -y install [email protected] [email protected] nimoji nimpy karax@#head"

task test, "General tests":
exec "nim r -d:nimibPreviewCodeAsInSource tests/tsources.nim"
Expand All @@ -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"
38 changes: 34 additions & 4 deletions src/nimib.nim
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -124,6 +124,36 @@ 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

template nbKaraxCode*(args: varargs[untyped]) =
let rootId = "karax-" & $nb.newId()
nbRawOutput: "<div id=\"" & rootId & "\"></div>"
nbKaraxCodeBackend(rootId, args)

template nbCodeToJsShowSource* =
nb.blk.context["js_show_nim_source"] = true


template nbClearOutput*() =
if not nb.blk.isNil:
nb.blk.output = ""
Expand Down
10 changes: 10 additions & 0 deletions src/nimib/boost.nim
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,14 @@ 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.
pietroppeter marked this conversation as resolved.
Show resolved Hide resolved
&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.
## Equivilent to ``&s`` so it applies string interpolation.
&s
Loading