Compare commits

..

18 commits

Author SHA1 Message Date
Roman Vishnevskii
78a47158a1 Introduce track field 2017-05-04 14:35:44 +07:00
Roman Vishnevskii
e63a7dbb21 Delete useless comment 2017-05-03 14:48:36 +07:00
Roman Vishnevskii
cd78f5085f Simplify updateElement 2017-05-02 19:50:35 +07:00
Roman Vishnevskii
4c582d7c06 Avoid using node.childNodes 2017-05-02 19:46:20 +07:00
Andreas Rumpf
d3e1d28c89 optimized VDOM implementation for uibench 2017-05-01 09:58:10 +02:00
Andreas Rumpf
91679f8ae6 made it shorter again 2017-04-27 18:23:29 +02:00
Andreas Rumpf
8bdaab3f82 uibenchmark works 2017-04-27 17:57:04 +02:00
Andreas Rumpf
ebccc4e9a1 simplifications 2017-04-27 17:39:54 +02:00
Andreas Rumpf
ed70a0cbfc Merge branch 'uibench' of github.com:pragmagic/karax into uibench 2017-04-27 17:27:22 +02:00
Andreas Rumpf
393c336f90 simplifications 2017-04-27 17:26:01 +02:00
Roman Vishnevskii
e434517dd1 Uibench final fix 2017-04-27 22:22:18 +07:00
Roman Vishnevskii
2b9880a9a2 Uibench fix border-radius 2017-04-27 21:58:47 +07:00
Roman Vishnevskii
f5e9db55c3 Change className to class 2017-04-27 20:57:33 +07:00
Roman Vishnevskii
a304322286 Uibench fix td 2017-04-27 20:49:54 +07:00
Andreas Rumpf
443b24de3e more fixes 2017-04-27 14:16:34 +02:00
Andreas Rumpf
41df9b769f more progress 2017-04-27 14:05:11 +02:00
Andreas Rumpf
1ad978804c fixes 2017-04-27 13:47:00 +02:00
Roman Vishnevskii
23fc331626 Add uibench 2017-04-27 18:13:25 +07:00
98 changed files with 1933 additions and 18655 deletions

View file

@ -1,31 +0,0 @@
# This is a basic workflow to help you get started with Actions
name: CI
# Controls when the action will run.
on:
# Triggers the workflow on push or pull request events but only for the master branch
push:
branches: [ master ]
pull_request:
branches: [ master ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
container: nimlang/nim
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
# Runs a single command using the runners shell
- name: Tests
run: |
nimble develop -y
nim c -r tests/tester.nim

6
.gitignore vendored
View file

@ -1,7 +1 @@
nimcache/
/tests/tester
karax/tools/karun
*.code-workspace
*.exe
app.js
app.html

View file

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2017 Xored Software, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -10,17 +10,17 @@ and some extensions that allow for an efficient component system.
These extensions are:
``VNodeKind.int``
The node has a single integer field.
The node havs a single integer field.
``VNodeKind.bool``
The node has a single boolean field.
``VNodeKind.vthunk``
``VNodeKint.vthunk``
The node is a `virtual thunk`:idx:. This means there is a
function attached to it that produces the ``VNode`` structure
on demand.
``VNodeKind.dthunk``
``VNodeKint.dthunk``
The node is a `DOM thunk`:idx:. This means there is a
function attached to it that produces the ``Node`` DOM structure
on demand.
@ -53,6 +53,6 @@ Event system
Karax does not abstract over the event system the DOM offers much: The same
``Event`` structure is used. Every callback has the
signature ``proc (ev: Event; n: VNode)`` or empty ``proc ()``.
signature ``proc (ev: Event; n: VNode)``.

View file

@ -1,15 +0,0 @@
include karax / prelude
var lines: seq[kstring] = @[]
proc createDom(): VNode =
result = buildHtml(tdiv):
button:
text "Say hello!"
proc onclick(ev: Event; n: VNode) =
lines.add "Hello simulated universe"
for x in lines:
tdiv:
text x
setRenderer createDom

View file

@ -1,14 +0,0 @@
include karax / prelude
from future import `=>`
var lines: seq[kstring] = @[]
proc createDom(): VNode =
result = buildHtml(tdiv):
button(onclick = () => lines.add "Hello simulated universe"):
text "Say hello!"
for x in lines:
tdiv:
text x
setRenderer createDom

View file

@ -1,15 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Carousel app</title>
</head>
<body id="body">
<div id="ROOT" />
<script type="text/javascript" src="carousel.js"></script>
</body>
</html>

View file

@ -1,69 +0,0 @@
## This demo shows how you can develop your own stateful components with Karax.
import vdom, vstyles, karax, karaxdsl, jdict, jstrutils, kdom
type
Carousel = ref object of VComponent
counter: int
cntdown: int
timer: TimeOut
list: seq[cstring]
const ticksUntilChange = 5
var
images: seq[cstring] = @[cstring"a", "b", "c", "d"]
proc render(x: VComponent): VNode =
let self = Carousel(x)
proc docount() =
dec self.cntdown
if self.cntdown == 0:
self.counter = (self.counter + 1) mod self.list.len
self.cntdown = ticksUntilChange
else:
self.timer = setTimeout(docount, 30)
markDirty(self)
redraw()
proc onclick(ev: Event; n: VNode) =
if self.timer != nil:
clearTimeout(self.timer)
self.timer = setTimeout(docount, 30)
let col =
case self.counter
of 0: cstring"#4d4d4d"
of 1: cstring"#ff00ff"
of 2: cstring"#00ffff"
of 3: cstring"#ffff00"
else: cstring"red"
result = buildHtml(tdiv()):
text self.list[self.counter]
button(onclick = onclick):
text "Next"
tdiv(style = style(StyleAttr.color, col)):
text "This changes its color."
if self.cntdown != ticksUntilChange:
text &self.cntdown
proc carousel(): Carousel =
result = newComponent(Carousel, render)
result.list = images
result.cntdown = ticksUntilChange
proc createDom(): VNode =
result = buildHtml(table):
tr:
td:
carousel()
td:
carousel()
tr:
td:
carousel()
td:
carousel()
setRenderer createDom

View file

@ -1 +0,0 @@
--path: "../../karax"

View file

@ -1 +0,0 @@
--path: ".."

View file

@ -1,17 +0,0 @@
include karax / prelude
import karax / vstyles
proc createDom(): VNode =
result = buildHtml(tdiv):
tdiv(style = style(StyleAttr.color, "red".cstring)):
text "red"
tdiv(style = style(StyleAttr.color, "blue")):
text "blue"
# explicit `kstring` required for varargs overload
tdiv(style = style((fontStyle, "italic".kstring), (color, "orange".kstring))):
text "italic orange"
# can use a string directly
tdiv(style = "font-style: oblique; color: pink".toCss):
text "oblique pink"
setRenderer createDom

View file

@ -1,12 +0,0 @@
include karax / prelude
import random
proc createDom(): VNode =
result = buildHtml(tdiv):
if rand(100) <= 50:
text "Hello World!"
else:
text "Hello Universe"
randomize()
setRenderer createDom

View file

@ -1,7 +0,0 @@
include karax / prelude
proc createDom(): VNode =
result = buildHtml(tdiv):
text "Hello World!"
setRenderer createDom

View file

@ -1,46 +0,0 @@
include karax / prelude
from sugar import `=>`
import karax / errors
proc loginField(desc, field, class: kstring;
validator: proc (field: kstring): proc ()): VNode =
result = buildHtml(tdiv):
label(`for` = field):
text desc
input(class = class, id = field, onchange = validator(field))
# some consts in order to prevent typos:
const
username = kstring"username"
password = kstring"password"
proc validateNotEmpty(field: kstring): proc () =
result = proc () =
let x = getVNodeById(field)
if x.text == "":
errors.setError(field, field & " must not be empty")
else:
errors.setError(field, "")
var loggedIn: bool
proc loginDialog(): VNode =
result = buildHtml(tdiv):
if not loggedIn:
loginField("Name: ", username, "input", validateNotEmpty)
loginField("Password: ", password, "password", validateNotEmpty)
button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()):
text "Login"
p:
text errors.getError(username)
p:
text errors.getError(password)
else:
p:
text "You are now logged in."
setError username, username & " must not be empty"
setError password, password & " must not be empty"
when not declared(toychat):
setRenderer loginDialog

View file

@ -1,5 +1,5 @@
import karax / [karax, karaxdsl, vdom, kdom, compact]
import karax, karaxdsl, vdom, dom, components
const
Play = 0
@ -12,10 +12,10 @@ proc play(n: Node) {.importcpp.}
proc pause(n: Node) {.importcpp.}
proc `width=`(n: Node, w: int) {.importcpp: "#.width = #".}
proc mplayer*(id, resource: cstring): VNode {.compact.} =
proc mplayer*(id, resource: cstring): VNode {.component.} =
proc handler(ev: Event; n: VNode) =
let myVideo = document.getElementById(id)
case n.index
case n.key
of Play:
if myVideo.paused:
myVideo.play()
@ -27,13 +27,13 @@ proc mplayer*(id, resource: cstring): VNode {.compact.} =
else: discard
result = buildHtml(tdiv):
button(onclick=handler, index=Play):
button(onclick=handler, key=Play):
text "Play/Pause"
button(onclick=handler, index=Big):
button(onclick=handler, key=Big):
text "Big"
button(onclick=handler, index=Small):
button(onclick=handler, key=Small):
text "Small"
button(onclick=handler, index=Normal):
button(onclick=handler, key=Normal):
text "Normal"
br()
br()

View file

@ -9,7 +9,7 @@
<div id="ROOT" />
<script type="text/javascript" src="playerapp.js"></script>
<script type="text/javascript" src="nimcache/playerapp.js"></script>
</body>
</html>

View file

@ -1,6 +1,6 @@
## Example app that shows how to write and embed a custom component.
include karax/prelude
include karaxprelude
import mediaplayer
const url = "https://www.w3schools.com/html/mov_bbb.mp4"

View file

@ -1 +1 @@
--path: "../.."
--path: "../../src"

View file

@ -9,7 +9,7 @@
<div id="ROOT" />
<script type="text/javascript" src="scrollapp.js"></script>
<script type="text/javascript" src="nimcache/scrollapp.js"></script>
</body>
</html>

View file

@ -1,7 +1,7 @@
## Example that shows how to accomplish an "infinitely scrolling" app.
include karax/prelude
import karax / [jstrutils, kdom, vstyles]
include karaxprelude
import jstrutils, dom
var entries: seq[cstring] = @[]
for i in 1..500:
@ -16,8 +16,7 @@ proc scrollEvent(ev: Event; n: VNode) =
proc createDom(): VNode =
result = buildHtml():
tdiv(onscroll=scrollEvent, style=style(
(StyleAttr.height, cstring"400px"), (StyleAttr.overflow, cstring"scroll"))):
tdiv(onscroll=scrollEvent, style="height: 400px; overflow: scroll"):
for x in entries:
tdiv:
text x

View file

@ -1 +1 @@
--path: "../.."
--path: "../../src"

View file

@ -10,7 +10,7 @@
<div id="ROOT" />
<script type="text/javascript" src="todoapp.js"></script>
<script type="text/javascript" src="nimcache/todoapp.js"></script>
</body>
</html>

View file

@ -1,5 +1,5 @@
import karax / [vdom, karax, karaxdsl, jstrutils, compact, localstorage]
import vdom, karax, karaxdsl, jstrutils, components, localstorage
type
Filter = enum
@ -9,7 +9,7 @@ var
selectedEntry = -1
filter: Filter
entriesLen: int
doneswitch = true
const
contentSuffix = cstring"content"
completedSuffix = cstring"completed"
@ -41,30 +41,29 @@ proc updateEntry(pos: int, content: cstring, completed: bool) =
markAsCompleted(pos, completed)
proc onTodoEnter(ev: Event; n: VNode) =
if n.value.strip() != "":
addEntry(n.value, false)
n.value = ""
proc removeHandler(ev: Event; n: VNode) =
updateEntry(n.index, cstring(nil), false)
updateEntry(n.key, cstring(nil), false)
proc editHandler(ev: Event; n: VNode) =
selectedEntry = n.index
selectedEntry = n.key
proc focusLost(ev: Event; n: VNode) = selectedEntry = -1
proc editEntry(ev: Event; n: VNode) =
setEntryContent(n.index, n.value)
setEntryContent(n.key, n.value)
selectedEntry = -1
proc toggleEntry(ev: Event; n: VNode) =
let id = n.index
let id = n.key
markAsCompleted(id, not isCompleted(id))
proc onAllDone(ev: Event; n: VNode) =
for i in 0..<entriesLen:
markAsCompleted(i, doneswitch)
doneswitch = not doneswitch
clear()
selectedEntry = -1
proc clearCompleted(ev: Event, n: VNode) =
for i in 0..<entriesLen:
if isCompleted(i): setEntryContent(i, nil)
@ -72,25 +71,28 @@ proc clearCompleted(ev: Event, n: VNode) =
proc toClass(completed: bool): cstring =
(if completed: cstring"completed" else: cstring(nil))
proc toChecked(checked: bool): cstring =
(if checked: cstring"checked" else: cstring(nil))
proc selected(v: Filter): cstring =
(if filter == v: cstring"selected" else: cstring(nil))
proc createEntry(id: int; d: cstring; completed, selected: bool): VNode {.compact.} =
proc createEntry(id: int; d: cstring; completed, selected: bool): VNode {.component.} =
result = buildHtml(tr):
li(class=toClass(completed)):
if not selected:
tdiv(class = "view"):
input(class = "toggle", `type` = "checkbox", checked = toChecked(completed),
onclick=toggleEntry, index=id)
label(onDblClick=editHandler, index=id):
onclick=toggleEntry, key=id)
label(onDblClick=editHandler, key=id):
text d
button(class = "destroy", index=id, onclick=removeHandler)
button(class = "destroy", key=id, onclick=removeHandler)
else:
input(class = "edit", name = "title", index=id,
input(class = "edit", name = "title", key=id,
onblur = focusLost,
onkeyupenter = editEntry, value = d, setFocus=true)
onkeyupenter = editEntry, value = d, setFocus)
proc makeFooter(entriesCount, completedCount: int): VNode =
proc makeFooter(entriesCount, completedCount: int): VNode {.component.} =
result = buildHtml(footer(class = "footer")):
span(class = "todo-count"):
strong:
@ -109,23 +111,20 @@ proc makeFooter(entriesCount, completedCount: int): VNode =
button(class = "clear-completed", onclick = clearCompleted):
text "Clear completed (" & &completedCount & ")"
proc makeHeader(): VNode {.compact.} =
proc makeHeader(): VNode {.component.} =
result = buildHtml(header(class = "header")):
h1:
text "todos"
input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
onkeyupenter = onTodoEnter, setFocus)
proc createDom(data: RouterData): VNode =
if data.hashPart == "#/": filter = all
elif data.hashPart == "#/completed": filter = completed
elif data.hashPart == "#/active": filter = active
proc createDom(): VNode =
result = buildHtml(tdiv(class="todomvc-wrapper")):
section(class = "todoapp"):
makeHeader()
section(class = "main"):
input(class = "toggle-all", `type` = "checkbox", id = "toggle", onclick = onAllDone)
label(`for` = "toggle"):
input(class = "toggle-all", `type` = "checkbox", name = "toggle")
label(`for` = "toggle-all", onclick = onAllDone):
text "Mark all as complete"
var entriesCount = 0
var completedCount = 0
@ -145,6 +144,12 @@ proc createDom(data: RouterData): VNode =
inc entriesCount
makeFooter(entriesCount, completedCount)
setOnHashChange(proc(hash: cstring) =
if hash == "#/": filter = all
elif hash == "#/completed": filter = completed
elif hash == "#/active": filter = active
)
if hasItem(lenSuffix):
entriesLen = parseInt getItem(lenSuffix)
else:

View file

@ -1 +1 @@
--path: "../.."
--path: "../../src"

View file

@ -1,35 +0,0 @@
include login
const
message = "message"
type
TextMessage = ref object
name, content: kstring
var allMessages: seq[TextMessage] = @[]
proc doSendMessage() =
let inputField = getVNodeById(message)
allMessages.add(TextMessage(name: "you", content: inputField.getInputText))
inputField.setInputText ""
proc main(): VNode =
result = buildHtml(tdiv):
loginDialog()
tdiv:
table:
for m in allMessages:
tr:
td:
bold:
text m.name
td:
text m.content
tdiv:
if loggedIn:
label(`for` = message):
text "Message: "
input(class = "input", id = message, onkeyupenter = doSendMessage)
setRenderer main

View file

@ -9,5 +9,5 @@
<div id="ROOT" />
<script type="text/javascript" src="ajaxtest.js"></script>
<script type="text/javascript" src="nimcache/ajaxtest.js"></script>
</html>

View file

@ -1,4 +1,4 @@
import kdom, kajax
import dom, kajax
proc cb(httpStatus: int, response: cstring) =
echo "Worked!"

View file

@ -1,2 +0,0 @@
--path: "../karax"
--path: ".."

View file

@ -1,14 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Karax ECharts Test</title>
</head>
<body id="body">
<div id="ROOT" />
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/echarts/4.0.2/echarts-en.js"></script>
<script type="text/javascript" src="echartstest.js"></script>
</html>

View file

@ -1,58 +0,0 @@
import karax / [kbase, vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, jjson]
type
Views = enum
Customers, Products
var
currentView = Customers
type
EChart* = ref object
proc echartsInit(n: Element): EChart {.importc: "echarts.init".}
proc setOption(x: EChart; option: JsonNode) {.importcpp.}
proc postRender(data: RouterData) =
if currentView == Products:
let myChart = echartsInit(kdom.getElementById("echartSection"))
# specify chart configuration item and data
let option = %*{
"title": {
"text": "ECharts entry example"
},
"tooltip": {},
"legend": {
"data": ["Sales"]
},
"xAxis": {
"data": ["shirt","cardign","chiffon shirt","pants","heels","socks"]
},
"yAxis": {},
"series": [{
"name": "Sales",
"type": "bar",
"data": [5, 20, 36, 10, 10, 20]
}]
}
myChart.setOption(option)
proc createDom(data: RouterData): VNode =
let hash = data.hashPart
if hash == cstring"#/Products": currentView = Products
else: currentView = Customers
result = buildHtml(tdiv):
ul(class = "tabs"):
for v in low(Views)..high(Views):
li:
a(href = "#/" & $v):
text cstring($v)
if currentView == Products:
tdiv(id = "echartSection", style = style((width, kstring"600px"), (height, kstring"400px")))
tdiv:
text "other section"
setRenderer createDom, "ROOT", postRender
setForeignNodeId "echartSection"

15
experiments/example.html Normal file
View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Todo app</title>
</head>
<body id="body">
<div id="ROOT" />
<script type="text/javascript" src="nimcache/example.js"></script>
</body>
</html>

47
experiments/example.nim Normal file
View file

@ -0,0 +1,47 @@
import vdom, components, karax, karaxdsl, jdict, jstrutils, dom
var
images: seq[cstring] = @[cstring"a", "b", "c", "d"]
proc carousel*(key: VKey): VNode {.component.} =
state:
var counter: int = 0
var cntdown: int = 5
var timer: Timeout = nil
proc docount() =
cntdown = cntdown - 1
if cntdown == 0:
counter = (counter + 1) mod images.len
cntdown = 5
else:
timer = setTimeout(docount, 30)
redraw()
proc next(ev: Event; n: VNode) =
if timer != nil:
clearTimeout(timer)
timer = setTimeout(docount, 30)
result = buildHtml(tdiv(key=key)):
text images[counter]
button(onclick = next):
text "Next"
if cntdown != 5:
text &cntdown
proc createDom(): VNode =
result = buildHtml(table):
tr:
td:
carousel(0)
td:
carousel(1)
tr:
td:
carousel(2)
td:
carousel(3)
setRenderer createDom

1
experiments/example.nims Normal file
View file

@ -0,0 +1 @@
--path: "../src"

View file

@ -1,65 +0,0 @@
## Example program that shows how to create menus with Karax.
include prelude
import jstrutils, kdom
proc contentA(): VNode =
result = buildHtml(tdiv):
text "content A"
proc contentB(): VNode =
result = buildHtml(tdiv):
text "content B"
proc contentC(): VNode =
result = buildHtml(tdiv):
text "content C"
type
MenuItemHandler = proc(): VNode
var content: MenuItemHandler = contentA
proc menuAction(x: MenuItemHandler): proc() =
result = proc() = content = x
proc buildMenu(): VNode =
result = buildHtml(tdiv):
nav(class="navbar is-primary"):
tdiv(class="navbar-brand"):
a(class="navbar-item", onclick = menuAction(contentA)):
strong:
text "My awesome menu"
tdiv(id="navMenuTransparentExample", class="navbar-menu"):
tdiv(class = "navbar-start"):
tdiv(class="navbar-item has-dropdown is-hoverable"):
a(class="navbar-link", onclick = menuAction(contentB)):
text "Masters"
tdiv(class="navbar-dropdown is-boxed"):
a(class="navbar-item", onclick = menuAction(contentC)):
text "Inventory"
a(class="navbar-item", onclick = menuAction(contentC)):
text "Product"
hr(class="navbar-divider"):
a(class="navbar-item", onclick = menuAction(contentC)):
text "Product"
tdiv(class="navbar-item has-dropdown is-hoverable"):
a(class="navbar-link", onclick = menuAction(contentC)):
text "Transactions"
tdiv(class = "navbar-dropdown is-boxed", id="blogDropdown"):
a(class="navbar-item", onclick = menuAction(contentC)):
text "Purchase Bill"
a(class="navbar-item", onclick = menuAction(contentC)):
text "Purchase Return"
a(class="navbar-item", onclick = menuAction(contentC)):
text "Sale Bill"
hr(class="navbar-divider"):
a(class="navbar-item", onclick = menuAction(contentC)):
text "Stock Adjustment"
content()
setRenderer buildMenu

View file

@ -1,18 +0,0 @@
include karax / prelude
import karax / kdom
proc myInput: VNode =
result = buildHtml:
input()
var inp = vnodeToDom(myInput())
proc myAwesomeComponent(x: Node): VNode =
result = dthunk(x)
proc main: VNode =
result = myAwesomeComponent(inp)
setRenderer main

View file

@ -1,120 +0,0 @@
import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, reactive
proc textInput*(text: RString; focus: RBool): VNode {.track.} =
proc onFlip(ev: Event; target: VNode) =
focus <- not focus.value
proc onKeyupEnter(ev: Event; target: VNode) =
text <- target.value
proc onkeyup(ev: Event; n: VNode) =
# keep displayValue up to date, but do not tell the client yet!
text.value = n.value
result = buildHtml(input(`type`="text",
value=text.value, onblur=onFlip, onfocus=onFlip,
onkeyupenter=onkeyupenter, onkeyup=onkeyup, setFocus=focus.value))
var
errmsg = rstr("")
makeReactive:
type
User = ref object
firstname, lastname: cstring
selected: bool
var gu = newRSeq(@[ (User(rawFirstname: "Some", rawLastname: "Body")),
(User(rawFirstname: "Some", rawLastname: "One")),
(User(rawFirstname: "Some", rawLastname: "Two"))])
var prevSelected: User = nil #newReactive[User](nil)
proc unselect() =
if prevSelected != nil:
prevSelected.selected = false
prevSelected = nil
proc select(u: User) =
unselect()
u.selected = true
prevSelected = u
proc toUI*(isFirstname: bool): RString =
result = RString()
result.subscribe proc (v: cstring) =
if v.len > 0:
let p = prevSelected #selected.value
if p != nil:
if isFirstName:
p.firstname = v
else:
p.lastname = v
unselect()
errmsg <- ""
else:
errmsg <- "name must not be empty"
var inpFirstname = toUI(true)
var inpLastname = toUI(false)
proc adaptFocus(def = false): RBool =
result = RBool()
result.value = def
when false:
result.subscribe proc (hasFocus: bool) =
if not hasFocus:
unselect()
var focusA = adaptFocus()
var focusB = adaptFocus()
proc styler(): VStyle =
result = style(
(StyleAttr.position, cstring"relative"),
(StyleAttr.paddingLeft, cstring"10px"),
(StyleAttr.paddingRight, cstring"5px"),
(StyleAttr.height, cstring"30px"),
(StyleAttr.lineHeight, cstring"30px"),
(StyleAttr.border, cstring"solid 8px " & (if focusA.value: cstring"red" else: cstring"black")),
(StyleAttr.fontSize, cstring"12px"),
(StyleAttr.fontWeight, cstring"600")
)
var clicks = 0
proc renderUser(u: User): VNode {.track.} =
result = buildHtml(tdiv):
if u.selected:
inpFirstname <- u.firstname
tdiv:
textInput inpFirstname, focusA
inpLastname <- u.lastname
tdiv:
textInput inpLastname, focusB
else:
button:
text "..."
proc onclick(ev: Event; n: VNode) =
select(u)
text u.firstname & " " & u.lastname
button:
text "(x)"
proc onclick(ev: Event; n: VNode) =
gu.deleteElem(u)
proc main(gu: RSeq[User]): VNode =
result = buildHtml(tdiv):
tdiv:
button:
text "Add User"
proc onclick(ev: Event; n: VNode) =
inc clicks
gu.add User(rawFirstname: "Added", rawLastname: &clicks)
tdiv:
text errmsg
vmapIt(gu, tdiv, renderUser(it))
proc init(): VNode = main(gu)
setInitializer(init)

View file

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charSet="utf-8"/>
<title>Refs</title>
</head>
<body id="body">
<div id="ROOT"></div>
<script type="text/javascript" src="demo.js"></script>
</body>
</html>

View file

@ -1,58 +0,0 @@
include karaxprelude
import future, sequtils
type
CustomRef = ref object of VComponent
method onAttach(r: CustomRef) =
kout(cstring"custom ref attached")
method onDetach(r: CustomRef) =
kout(cstring"custom ref detached")
var
modelData = @[5, 2, 4]
refA = VComponent()
refB = VComponent()
refC = CustomRef()
refSeq = newSeq[VComponent]()
proc onClick(ev: Event, n: VNode) =
modelData.add(0)
proc showRefs() =
kout(refA)
kout(refB)
kout(refC)
for i in 0 ..< refSeq.len:
kout(refSeq[i].vnode)
#kout(refSeq.map(nref => nref.vnode))
proc secureRefSlot(i: int): VComponent =
while refSeq.len <= i:
refSeq.add(VComponent())
result = refSeq[i]
proc view(): VNode =
result = buildHtml():
tdiv:
button(onclick=onClick):
text "click me"
# storing refs to single elements is straightforward now
if modelData.len mod 2 == 0:
tdiv(nref=refA):
text "A"
tdiv(nref=refB):
text "B"
tdiv(nref=refC):
text "C"
# It's a bit more tricky when containers are involved:
for i, x in modelData.pairs:
tdiv(nref=secureRefSlot(i)):
text "List item: " & $x
proc renderer(): VNode =
view()
setRenderer(renderer, "ROOT", showRefs)

View file

@ -1 +0,0 @@
--path: "../../src"

View file

@ -10,7 +10,7 @@
<div id="ROOT" />
<script type="text/javascript" src="scrollapp.js"></script>
<script type="text/javascript" src="nimcache/scrollapp.js"></script>
<script type="text/javascript">
onload(window.location.pathname.split('/')[1])
</script>

View file

@ -1,5 +1,5 @@
import vdom, karax, karaxdsl, jdict, jstrutils, compact
import vdom, karax, karaxdsl, jdict, jstrutils, components
type
Filter = enum

View file

@ -0,0 +1 @@
--path: "../src"

View file

@ -1,25 +0,0 @@
## Example that shows how to accomplish an "infinitely scrolling" app.
include karax / prelude
import karax / [kdom, vstyles]
var entries: seq[cstring] = @[]
for i in 1..500:
entries.add(cstring("Entry ") & &i)
proc scrollEvent(ev: Event; n: VNode) =
let d = n.dom
if d != nil and inViewport(d.lastChild):
# "load" more data:
for i in 1..50:
entries.add(cstring("Loaded Entry ") & &i)
proc createDom(): VNode =
result = buildHtml():
tdiv(onscroll=scrollEvent, style=style(
(StyleAttr.height, cstring"400px"), (StyleAttr.overflow, cstring"scroll"))):
for x in entries:
tdiv:
text x
setRenderer createDom

View file

@ -1,752 +0,0 @@
## Raw DOM manipulation. No DOM diff'ing, no cry.
import macros, tables
from strutils import startsWith, toLowerAscii, toUpperAscii
when defined(js):
type kstring* = cstring
else:
type kstring* = string
type
Tag* {.pure.} = enum
text = "#text",
html, head, title, base, link, meta, style,
script, noscript,
body, section, nav, article, aside,
h1, h2, h3, h4, h5, h6,
header, footer, address, main
p, hr, pre, blockquote, ol, ul, li,
dl, dt, dd,
figure, figcaption,
tdiv = "div",
a, em, strong, small,
strikethrough = "s", cite, quote,
dfn, abbr, data, time, code, `var` = "var", samp,
kdb, sub, sup, italic = "i", bold = "b", underlined = "u",
mark, ruby, rt, rp, bdi, dbo, span, br, wbr,
ins, del, img, iframe, embed, `object` = "object",
param, video, audio, source, track, canvas, map,
area, svg, math,
table, caption, colgroup, col, tbody, thead,
tfoot, tr, td, th,
form, fieldset, legend, label, input, button,
select, datalist, optgroup, option, textarea,
keygen, output, progress, meter,
details, summary, command, menu
type
EventKind* {.pure.} = enum ## The events supported by the virtual DOM.
onclick, ## An element is clicked.
oncontextmenu, ## An element is right-clicked.
ondblclick, ## An element is double clicked.
onkeyup, ## A key was released.
onkeydown, ## A key is pressed.
onkeypressed, # A key was pressed.
onfocus, ## An element got the focus.
onblur, ## An element lost the focus.
onchange, ## The selected value of an element was changed.
onscroll, ## The user scrolled within an element.
onmousedown, ## A pointing device button (usually a mouse) is pressed
## on an element.
onmouseenter, ## A pointing device is moved onto the element that
## has the listener attached.
onmouseleave, ## A pointing device is moved off the element that
## has the listener attached.
onmousemove, ## A pointing device is moved over an element.
onmouseout, ## A pointing device is moved off the element that
## has the listener attached or off one of its children.
onmouseover, ## A pointing device is moved onto the element that has
## the listener attached or onto one of its children.
onmouseup, ## A pointing device button is released over an element.
ondrag, ## An element or text selection is being dragged (every 350ms).
ondragend, ## A drag operation is being ended (by releasing a mouse button
## or hitting the escape key).
ondragenter, ## A dragged element or text selection enters a valid drop target.
ondragleave, ## A dragged element or text selection leaves a valid drop target.
ondragover, ## An element or text selection is being dragged over a valid
## drop target (every 350ms).
ondragstart, ## The user starts dragging an element or text selection.
ondrop, ## An element is dropped on a valid drop target.
onsubmit, ## A form is submitted
oninput, ## An input value changes
onkeyupenter, ## vdom extension: an input field received the ENTER key press
onkeyuplater ## vdom extension: a key was pressed and some time
## passed (useful for on-the-fly text completions)
macro buildLookupTables(): untyped =
var a = newTree(nnkBracket)
for i in low(Tag)..high(Tag):
let x = $i
let y = if x[0] == '#': x else: toUpperAscii(x)
a.add(newCall("kstring", newLit(y)))
var e = newTree(nnkBracket)
for i in low(EventKind)..high(EventKind):
e.add(newCall("kstring", newLit(substr($i, 2))))
template tmpl(a, e) {.dirty.} =
const
toTagName*: array[Tag, kstring] = a
toEventName*: array[EventKind, kstring] = e
result = getAst tmpl(a, e)
buildLookupTables()
# ---------------- CSS ----------------------------------------------
type
StyleAttr* {.pure.} = enum
## The style attributes supported by the virtual DOM.
## Reference: https://www.w3schools.com/jsref/dom_obj_style.asp
alignContent
alignItems
alignSelf
animation
animationDelay
animationDirection
animationDuration
animationFillMode
animationIterationCount
animationName
animationTimingFunction
animationPlayState
background
backgroundAttachment
backgroundColor
backgroundImage
backgroundPosition
backgroundRepeat
backgroundClip
backgroundOrigin
backgroundSize
backfaceVisibility
border
borderBottom
borderBottomColor
borderBottomLeftRadius
borderBottomRightRadius
borderBottomStyle
borderBottomWidth
borderCollapse
borderColor
borderImage
borderImageOutset
borderImageRepeat
borderImageSlice
borderImageSource
borderImageWidth
borderLeft
borderLeftColor
borderLeftStyle
borderLeftWidth
borderRadius
borderRight
borderRightColor
borderRightStyle
borderRightWidth
borderSpacing
borderStyle
borderTop
borderTopColor
borderTopLeftRadius
borderTopRightRadius
borderTopStyle
borderTopWidth
borderWidth
bottom
boxDecorationBreak
boxShadow
boxSizing
captionSide
clear
clip
color
columnCount
columnFill
columnGap
columnRule
columnRuleColor
columnRuleStyle
columnRuleWidth
columns
columnSpan
columnWidth
content
counterIncrement
counterReset
cursor
direction
display
emptyCells
filter
flex
flexBasis
flexDirection
flexFlow
flexGrow
flexShrink
flexWrap
cssFloat
font
fontFamily
fontSize
fontSizeAdjust
fontStretch
fontStyle
fontVariant
fontWeight
hangingPunctuation
height
hyphens
icon
imageOrientation
justifyContent
left
letterSpacing
lineHeight
listStyle
listStyleImage
listStylePosition
listStyleType
margin
marginBottom
marginLeft
marginRight
marginTop
maxHeight
maxWidth
minHeight
minWidth
navDown
navIndex
navLeft
navRight
navUp
opacity
order
orphans
outline
outlineColor
outlineOffset
outlineStyle
outlineWidth
overflow
overflowX
overflowY
padding
paddingBottom
paddingLeft
paddingRight
paddingTop
pageBreakAfter
pageBreakBefore
pageBreakInside
perspective
perspectiveOrigin
pointerEvents
position
quotes
resize
right
scrollbar3dLightColor # note: missing in w3schools reference
scrollbarArrowColor # note: missing in w3schools reference
scrollbarBaseColor # note: missing in w3schools reference
scrollbarDarkshadowColor # note: missing in w3schools reference
scrollbarFaceColor # note: missing in w3schools reference
scrollbarHighlightColor # note: missing in w3schools reference
scrollbarShadowColor # note: missing in w3schools reference
scrollbarTrackColor # note: missing in w3schools reference
tableLayout
tabSize
textAlign
textAlignLast
textDecoration
textDecorationColor
textDecorationLine
textDecorationStyle
textIndent
textJustify
textOverflow
textShadow
textTransform
top
transform
transformOrigin
transformStyle
transition
transitionDelay
transitionDuration
transitionProperty
transitionTimingFunction
unicodeBidi
userSelect
verticalAlign
visibility
whiteSpace
width
wordBreak
wordSpacing
wordWrap
widows
zIndex
macro buildStyleLookupTable(): untyped =
var e = newTree(nnkBracket)
for i in low(StyleAttr)..high(StyleAttr):
e.add(newCall("kstring", newLit($i)))
template tmpl(e) {.dirty.} =
const
toStyleAttrName: array[StyleAttr, kstring] = e
result = getAst tmpl(e)
buildStyleLookupTable()
type
StyleObj {.importc.} = object
background*: cstring
backgroundAttachment*: cstring
backgroundColor*: cstring
backgroundImage*: cstring
backgroundPosition*: cstring
backgroundRepeat*: cstring
border*: cstring
borderBottom*: cstring
borderBottomColor*: cstring
borderBottomStyle*: cstring
borderBottomWidth*: cstring
borderColor*: cstring
borderLeft*: cstring
borderLeftColor*: cstring
borderLeftStyle*: cstring
borderLeftWidth*: cstring
borderRight*: cstring
borderRightColor*: cstring
borderRightStyle*: cstring
borderRightWidth*: cstring
borderStyle*: cstring
borderTop*: cstring
borderTopColor*: cstring
borderTopStyle*: cstring
borderTopWidth*: cstring
borderWidth*: cstring
bottom*: cstring
captionSide*: cstring
clear*: cstring
clip*: cstring
color*: cstring
cursor*: cstring
direction*: cstring
display*: cstring
emptyCells*: cstring
cssFloat*: cstring
font*: cstring
fontFamily*: cstring
fontSize*: cstring
fontStretch*: cstring
fontStyle*: cstring
fontVariant*: cstring
fontWeight*: cstring
height*: cstring
left*: cstring
letterSpacing*: cstring
lineHeight*: cstring
listStyle*: cstring
listStyleImage*: cstring
listStylePosition*: cstring
listStyleType*: cstring
margin*: cstring
marginBottom*: cstring
marginLeft*: cstring
marginRight*: cstring
marginTop*: cstring
maxHeight*: cstring
maxWidth*: cstring
minHeight*: cstring
minWidth*: cstring
overflow*: cstring
padding*: cstring
paddingBottom*: cstring
paddingLeft*: cstring
paddingRight*: cstring
paddingTop*: cstring
pageBreakAfter*: cstring
pageBreakBefore*: cstring
pointerEvents*: cstring
position*: cstring
right*: cstring
scrollbar3dLightColor*: cstring
scrollbarArrowColor*: cstring
scrollbarBaseColor*: cstring
scrollbarDarkshadowColor*: cstring
scrollbarFaceColor*: cstring
scrollbarHighlightColor*: cstring
scrollbarShadowColor*: cstring
scrollbarTrackColor*: cstring
tableLayout*: cstring
textAlign*: cstring
textDecoration*: cstring
textIndent*: cstring
textTransform*: cstring
transform*: cstring
top*: cstring
verticalAlign*: cstring
visibility*: cstring
width*: cstring
wordSpacing*: cstring
zIndex*: int
Style* = ref StyleObj
ElementObj {.importc.} = object
id*, class*, value*: cstring
disabled*: bool
style*: Style
Element* = ref ElementObj
DocumentObj {.importc.} = object
Document = ref DocumentObj
proc setStyle(s: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.}
proc applyStyles*(e: Element; pairs: openArray[(StyleAttr, kstring)]) =
for x in pairs:
e.style.setStyle(toStyleAttrName[x[0]], x[1])
# ------- Tree manipulation ---------------------------
var document {.importc.}: Document
proc parent*(x: Element): Element {.importcpp: "#.parentNode".}
proc up*(x: Element; className: cstring): Element =
result = x
while result != nil and result.class != className:
result = result.parent
proc len*(x: Element): int {.importcpp: "#.childNodes.length".}
proc `[]`*(x: Element; idx: int): Element {.importcpp: "#.childNodes[#]".}
proc getElementById*(id: cstring): Element {.importc: "document.getElementById", nodecl.}
proc add*(n, child: Element) {.importcpp: "appendChild".}
proc removeChild(n, child: Element) {.importcpp.}
proc replaceChild(n, newNode, oldNode: Element) {.importcpp.}
proc replace*(self, by: Element) = replaceChild(self.parent, by, self)
proc delete*(self: Element) = removeChild(self.parent, self)
proc insertBefore(n, newNode, before: Element) {.importcpp.}
proc insert*(before, newNode: Element) = insertBefore(before.parent, newNode, before)
proc createTextNode(d: Document, text: cstring): Element {.importcpp.}
proc text*(s: kstring): Element = createTextNode(document, s)
iterator items*(n: Element): Element =
for i in 0..<n.len: yield n[i]
proc createElement(d: Document, identifier: cstring): Element {.importcpp.}
proc setAttr*(n: Element; key, val: cstring) {.importcpp: "#.setAttribute(@)".}
proc tree*(kind: Tag; kids: varargs[Element]): Element =
result = createElement(document, toTagName[kind])
for k in kids: result.add k
proc tree*(kind: Tag; attrs: openarray[(kstring, kstring)];
kids: varargs[Element]): Element =
result = tree(kind, kids)
for a in attrs: result.setAttr(a[0], a[1])
# other arbitrary stuff belonging to Element
proc focus*(e: Element) {.importcpp.}
proc scrollIntoView*(e: Element) {.importcpp.}
proc blur*(e: Element) {.importcpp.}
proc toChecked*(checked: bool): cstring =
(if checked: cstring"checked" else: cstring(nil))
proc toDisabled*(disabled: bool): cstring =
(if disabled: cstring"disabled" else: cstring(nil))
# -------------- Timer handling --------------------
type
Timeout* {.importc.} = ref object of RootObj
proc setTimeout*(action: proc(); ms: int): Timeout {.importc, nodecl.}
proc clearTimeout*(t: Timeout) {.importc, nodecl.}
# -------------- Event handling --------------------
type
EventObj {.importc.} = object
target*: Element
altKey*, ctrlKey*, metaKey*, shiftKey*: bool
code*: cstring
isComposing*: bool
key*: cstring
keyCode*: int
location*: int
Event* = ref EventObj
EventHandler* = proc (ev: Event) {.closure.}
proc addEventListener(e: Element, ev: cstring, cb: proc(ev: Event),
useCapture = false) {.importcpp.}
proc addEventHandler*(e: Element; k: EventKind; action: EventHandler) =
case k
of EventKind.onkeyuplater:
proc laterWrapper(): EventHandler =
let action = action
var timer: Timeout
result = proc (ev: Event) =
proc wrapper() = action(ev)
if timer != nil: clearTimeout(timer)
timer = setTimeout(wrapper, 400)
e.addEventListener("keyup", laterWrapper())
of EventKind.onkeyupenter:
proc enterWrapper(): EventHandler =
let action = action
result = proc (ev: Event) =
if ev.keyCode == 13: action(ev)
e.addEventListener("keyup", enterWrapper())
else:
e.addEventListener(toEventName[k], action)
proc addEventHandler*(e: Element; k: EventKind; action: proc()) =
addEventHandler e, k, (proc (ev: Event) = action())
proc prepareDragData*(ev: Event; datatype, data: cstring)
{.importcpp: "#.dataTransfer.setData(@)".}
proc recvDragData*(ev: Event; datatype: cstring): cstring
{.importcpp: "#.dataTransfer.getData(@)".}
proc preventDefault*(ev: Event) {.importcpp.}
# ------------------ Init handling -----------------------------------
proc replaceById*(newTree: Element; id: cstring = "ROOT") =
let x = getElementById(id)
x.parent.replaceChild(newTree, x)
newTree.id = id
proc setWindowOnload(h: EventHandler) {.importcpp: "window.onload = #".}
proc setInitializer*(initializer: proc (hashPart: kstring): Element;
root: cstring = "ROOT") =
var onhashChange {.importc: "window.onhashchange".}: proc()
var hashPart {.importc: "window.location.hash".}: cstring
setWindowOnload proc (ev: Event) =
replaceById root, initializer(hashPart)
onhashchange = proc () =
replaceById root, initializer(hashPart)
# ------------------ DSL section -------------------------------------
const
StmtContext = ["inc", "echo", "dec", "!"]
SpecialAttrs = ["id", "class", "value"]
type
ComponentKind {.pure.} = enum
None,
Tag
var
allcomponents {.compileTime.} = initTable[string, ComponentKind]()
proc isComponent(x: string): ComponentKind {.compileTime.} =
allcomponents.getOrDefault(x)
proc addTags() {.compileTime.} =
let x = (bindSym"Tag").getTypeImpl
expectKind(x, nnkEnumTy)
for i in ord(Tag.html)..ord(Tag.high):
# +1 because of empty node at the start of the enum AST:
let tag = $x[i+1]
allcomponents[tag] = ComponentKind.Tag
static:
addTags()
proc getName(n: NimNode): string =
case n.kind
of nnkIdent:
result = $n.ident
of nnkAccQuoted:
result = ""
for i in 0..<n.len:
result.add getName(n[i])
of nnkStrLit..nnkTripleStrLit:
result = n.strVal
else:
#echo repr n
expectKind(n, nnkIdent)
proc newDotAsgn(tmp: NimNode, key: string, x: NimNode): NimNode =
result = newTree(nnkAsgn, newDotExpr(tmp, newIdentNode key), x)
proc toKstring(n: NimNode): NimNode =
if n.kind == nnkStrLit:
result = newCall(bindSym"kstring", n)
else:
result = copyNimNode(n)
for child in n:
result.add toKstring(child)
proc tcall2(n, tmpContext: NimNode): NimNode =
# we need to distinguish statement and expression contexts:
# every call statement 's' needs to be transformed to 'dest.add s'.
# If expressions need to be distinguished from if statements. Since
# we know we start in a statement context, it's pretty simple to
# figure out expression contexts: In calls everything is an expression
# (except for the last child of the macros we consider here),
# lets, consts, types can be considered as expressions
# case is complex, calls are assumed to produce a value.
when defined(js):
template evHandler(): untyped = bindSym"addEventHandler"
else:
template evHandler(): untyped = ident"addEventHandler"
case n.kind
of nnkLiterals, nnkIdent, nnkSym, nnkDotExpr, nnkBracketExpr:
if tmpContext != nil:
result = newCall(bindSym"add", tmpContext, n)
else:
result = n
of nnkForStmt, nnkIfExpr, nnkElifExpr, nnkElseExpr,
nnkOfBranch, nnkElifBranch, nnkExceptBranch, nnkElse,
nnkConstDef, nnkWhileStmt, nnkIdentDefs, nnkVarTuple:
# recurse for the last son:
result = copyNimTree(n)
let L = n.len
assert n.len == result.len
if L > 0:
result[L-1] = tcall2(result[L-1], tmpContext)
of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkTryStmt,
nnkFinally:
# recurse for every child:
result = copyNimNode(n)
for x in n:
result.add tcall2(x, tmpContext)
of nnkCaseStmt:
# recurse for children, but don't add call for case ident
result = copyNimNode(n)
result.add n[0]
for i in 1 ..< n.len:
result.add tcall2(n[i], tmpContext)
of nnkProcDef:
let name = getName n[0]
if name.startsWith"on":
# turn it into an anon proc:
let anon = copyNimTree(n)
anon[0] = newEmptyNode()
if tmpContext == nil:
error "no Element to attach the event handler to"
else:
result = newCall(evHandler(), tmpContext,
newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi"))
else:
result = n
of nnkVarSection, nnkLetSection, nnkConstSection:
result = n
of nnkCallKinds - {nnkInfix}:
let op = getName(n[0])
if isComponent(op) == ComponentKind.Tag:
let tmp = genSym(nskLet, "tmp")
let call = newCall(bindSym"tree", newDotExpr(bindSym"Tag", n[0]))
result = newTree(
if tmpContext == nil: nnkStmtListExpr else: nnkStmtList,
newLetStmt(tmp, call))
for i in 1 ..< n.len:
# named parameters are transformed into attributes or events:
let x = n[i]
if x.kind == nnkExprEqExpr:
let key = getName x[0]
if key.startsWith("on"):
result.add newCall(evHandler(),
tmp, newDotExpr(bindSym"EventKind", x[0]), x[1])
elif key in SpecialAttrs:
result.add newDotAsgn(tmp, key, x[1])
elif eqIdent(key, "style"):
result.add newCall(bindSym"applyStyles", tmp, toKstring x[1])
elif eqIdent(key, "setFocus"):
result.add newCall(bindSym"focus", tmp)
else:
result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
elif eqIdent(x, "setFocus"):
result.add newCall(bindSym"focus", tmp)
else:
result.add tcall2(x, tmp)
if tmpContext == nil:
result.add tmp
else:
result.add newCall(bindSym"add", tmpContext, tmp)
elif tmpContext != nil and op notin StmtContext:
var hasEventHandlers = false
for i in 1..<n.len:
let it = n[i]
if it.kind in {nnkProcDef, nnkStmtList}:
hasEventHandlers = true
break
if not hasEventHandlers:
result = newCall(bindSym"add", tmpContext, n)
else:
let tmp = genSym(nskLet, "tmp")
var slicedCall = newCall(n[0])
let ex = newTree(nnkStmtListExpr)
ex.add newEmptyNode() # will become the let statement
for i in 1..<n.len:
let it = n[i]
if it.kind in {nnkProcDef, nnkStmtList}:
ex.add tcall2(it, tmp)
else:
slicedCall.add it
ex[0] = newLetStmt(tmp, slicedCall)
ex.add tmp
result = newCall(bindSym"add", tmpContext, ex)
elif op == "!" and n.len == 2:
result = n[1]
else:
result = n
else:
result = n
macro buildHtml*(tag, children: untyped): Element =
let kids = newProc(procType=nnkDo, body=children)
expectKind kids, nnkDo
var call: NimNode
if tag.kind in nnkCallKinds:
call = tag
else:
call = newCall(tag)
call.add body(kids)
result = tcall2(call, nil)
when defined(debugKaraxDsl):
echo repr result
macro buildHtml*(children: untyped): Element =
let kids = newProc(procType=nnkDo, body=children)
expectKind kids, nnkDo
result = tcall2(body(kids), nil)
when defined(debugKaraxDsl):
echo repr result
macro flatHtml*(tag: untyped): Element =
result = tcall2(tag, nil)
when defined(debugKaraxDsl):
echo repr result

View file

@ -1,151 +0,0 @@
import knete, widgets
import karax / jstrutils
type
Attachable* = ref object of RootObj ## an element that is attachable to
## DOM elements.
attachedTo*: seq[Element]
TaskId = int
Task = ref object
id: TaskId
name, desc: kstring
ColumnId = distinct int
Column = ref object of Attachable
id: ColumnId
header: kstring
tasks: seq[Task]
Board = ref object
columns: seq[Column]
var b = Board(columns: @[Column(id: ColumnId 0, header: "To Do"),
Column(id: ColumnId 1, header: "Doing"),
Column(id: ColumnId 2, header: "Done")])
proc removeTask(t: TaskId) =
for c in b.columns:
for i in 0 ..< c.tasks.len:
if c.tasks[i].id == t:
delete(c.tasks, i)
break
proc moveTask(t: TaskId; dest: Column) =
for c in b.columns:
for i in 0 ..< c.tasks.len:
let task = c.tasks[i]
if task.id == t:
delete(c.tasks, i)
dest.tasks.add task
return
var nextTaskId = 0 ## negative because they are not yet backed
## up by the database
proc createTask(c: Column; name, desc: kstring): Task =
dec nextTaskId
result = Task(id: TaskId nextTaskId, name: name, desc: desc)
c.tasks.add result
# --------------------- UI -----------------------------------------------
proc renderTask(t: Task): Element =
proc dodelete =
removeTask t.id
delete result
proc dragstart(ev: Event) =
ev.prepareDragData("taskid", ev.target.id)
result = buildHtml():
tdiv(draggable="true", ondragstart=dragstart, id = &t.id):
bindField t.name
br()
bindField t.desc
button(onclick = dodelete):
text cross
type
NewTaskPanel = object
c: Column
e: Element
proc open(p: var NewTaskPanel; c: Column) =
p.e.style.display = "block"
p.c = c
proc newTaskDialog(): NewTaskPanel =
var nameInp = buildHtml():
input(setFocus = true)
var descInp = buildHtml():
input()
proc close =
result.e.style.display = "none"
proc submit =
let t = createTask(result.c, nameInp.value, descInp.value)
close()
result.c.attachedTo[0].add renderTask(t)
result.e = buildHtml():
tdiv(style={display: "none", position: "fixed",
left: "0", top: "0", width: "100%", height: "100%",
overflow: "auto",
backgroundColor: "rgb(0,0,0)",
backgroundColor: "rgba(0,0,0,0.4)", zIndex: "1"}):
tdiv(style={backgroundColor: "#fefefe",
margin: "15% auto", padding: "20px", border: "1px solid #888",
width: "80%"}):
span(onclick = close, style={color: "#aaa",
cssFloat: "right",
fontSize: "28px",
fontWeight: "bold"}):
text cross
p:
text "Task name"
nameInp
p:
text "Task description"
descInp
span(onclick = submit, style={color: "#0f0",
cssFloat: "left",
fontWeight: "bold"}):
text "Submit"
var dialog = newTaskDialog()
proc renderColumn(c: Column): Element =
proc doadd(c: Column): proc () =
result = proc() = dialog.open(c)
proc allowDrop(ev: Event) = ev.preventDefault()
proc drop(ev: Event) =
ev.preventDefault()
let data = ev.recvDragData("taskid")
moveTask(parseInt data, c)
ev.target.up("mycolumn").add(getElementById(data))
result = buildHtml():
tdiv(class = "mycolumn", style = {cssFloat: "left", width: "20%"},
ondrop = drop, ondragover = allowDrop):
tdiv(class = "myheader"):
bindField c.header
span(onclick = doadd(c)):
text plus
for t in c.tasks:
renderTask(t)
c.attachedTo.add result
proc renderBoard(b: Board): Element =
result = buildHtml(tdiv):
dialog.e
for c in b.columns:
renderColumn(c)
proc main(hashPart: kstring): Element =
result = renderBoard(b)
setInitializer main

View file

@ -1,43 +0,0 @@
## Common widget implemenations
import knete
const
cross* = "x"
plus* = kstring"+"
const
LEFT* = 37
UP* = 38
RIGHT* = 39
DOWN* = 40
TAB* = 9
ESC* = 27
ENTER* = 13
proc editable*(x: kstring; onchanged: proc (value: kstring);
isEdited = false): Element =
proc onenter(ev: Event) = ev.target.blur()
proc submit(ev: Event) =
onchanged(ev.target.value)
# close the loop: This is a common pattern in Knete.
replace(result, editable(ev.target.value, onchanged, false))
proc makeEditable() =
# close the loop: This is a common pattern in Knete.
replace(result, editable(x, onchanged, true))
result = buildHtml():
if isEdited:
input(class = "edit",
onblur = submit,
onkeyupenter = onenter,
value = x,
setFocus = true)
else:
span(onclick = makeEditable):
text x
template bindField*(field): Element =
editable field, proc (value: kstring) =
field = value

View file

@ -1,16 +1,13 @@
# Package
version = "1.2.1"
author = "Andreas Rumpf"
version = "0.1.0"
author = "Xored Software, Inc."
description = "Karax is a framework for developing single page applications in Nim."
license = "MIT"
srcDir = "src"
# Dependencies
requires "nim >= 0.18.0"
requires "ws"
requires "dotenv"
skipDirs = @["examples", "experiments", "tests"]
requires "nim >= 0.16.1"
bin = @["karax/tools/karun"]
installExt = @["nim"]
skipDirs = @["examples/todoapp"]

View file

@ -1,160 +0,0 @@
import karax, karaxdsl, vdom, kdom, jstrutils
type
AutocompleteState* = ref object
choices, candidates: seq[cstring]
selected, maxMatches: int
showCandidates, controlPressed: bool
proc newAutocomplete*(choices: seq[cstring]; maxMatches = 5): AutocompleteState =
## Creates a new state for the autocomplete widget. ``maxMatches`` is the maximum
## number of elements to show.
AutocompleteState(choices: choices, candidates: @[],
selected: -1, maxMatches: maxMatches, showCandidates: false,
controlPressed: false)
proc autocomplete*(s: AutocompleteState; onselection: proc(s: cstring)): VNode =
var inp: VNode
proc commit(ev: Event) =
s.showCandidates = false
onselection(inp.dom.value)
when false:
if inp.dom != nil:
echo "setting to A ", inp.dom.value.isNil
result.text = inp.dom.value
else:
echo "setting to B ", inp.text.isNil
result.text = inp.text
for e in result.events:
if e[0] == EventKind.onchange:
e[1](ev, result)
proc onkeyuplater(ev: Event; n: VNode) =
if not s.controlPressed:
let v = n.value
s.candidates.setLen 0
for c in s.choices:
if v.len == 0 or c.containsIgnoreCase(v): s.candidates.add(c)
proc onkeydown(ev: Event; n: VNode) =
const
LEFT = 37
UP = 38
RIGHT = 39
DOWN = 40
TAB = 9
ESC = 27
ENTER = 13
# UP: Move focus to the previous item. If on first item, move focus to the input.
# If on the input, move focus to last item.
# DOWN: Move focus to the next item. If on last item, move focus to the input.
# If on the input, move focus to the first item.
# ESCAPE: Close the menu.
# ENTER: Select the currently focused item and close the menu.
# TAB: Select the currently focused item, close the menu, and
# move focus to the next focusable element
s.controlPressed = false
case ev.keyCode
of UP:
s.controlPressed = true
s.showCandidates = true
if s.selected > 0:
dec s.selected
n.setInputText s.candidates[s.selected]
of DOWN:
s.controlPressed = true
s.showCandidates = true
if s.selected < s.candidates.len - 1:
inc s.selected
n.setInputText s.candidates[s.selected]
of ESC:
s.showCandidates = false
s.controlPressed = true
of ENTER:
s.controlPressed = true
# inp.setInputText s.choices[i]
commit(ev)
else:
discard
proc window(s: AutocompleteState): (int, int) =
var first, last: int
if s.selected >= 0:
first = s.selected - (s.maxMatches div 2)
last = s.selected + (s.maxMatches div 2)
if first < 0: first = 0
# too few because we had to trim first?
if (last - first + 1) < s.maxMatches: last = first + s.maxMatches - 1
else:
first = 0
last = s.maxMatches - 1
if last > high(s.candidates): last = high(s.candidates)
# still too few because we're at the end?
if (last - first + 1) < s.maxMatches:
first = last - s.maxMatches + 1
if first < 0: first = 0
result = (first, last)
inp = buildHtml:
input(onkeyuplater = onkeyuplater,
onkeydown = onkeydown,
# onblur = proc (ev: Event; n: VNode) = commit(ev),
onfocus = proc (ev: Event; n: VNode) =
onkeyuplater(ev, n)
s.showCandidates = true)
proc select(i: int): proc(ev: Event; n: VNode) =
result = proc(ev: Event; n: VNode) =
s.selected = i
s.showCandidates = false
inp.setInputText s.choices[i]
commit(ev)
result = buildHtml(table):
tr:
td:
inp
if s.showCandidates:
let (first, last) = window(s)
for i in first..last:
tr:
td(onclick = select(i), class = cstring"button " &
(if i == s.selected: cstring"is-primary" else: "")):
text s.candidates[i]
when isMainModule:
const suggestions = @[cstring"ActionScript",
"AppleScript",
"Asp",
"BASIC",
"C",
"C++",
"Clojure",
"COBOL",
"Erlang",
"Fortran",
"Groovy",
"Haskell",
"Java",
"JavaScript",
"Lisp",
"Nim",
"Perl",
"PHP",
"Python",
"Ruby",
"Scala",
"Scheme"]
var s = newAutocomplete(suggestions)
proc main(): VNode =
result = buildHtml(tdiv):
autocomplete(s, proc (s: cstring) = echo "now ", s)#:
# proc onchange(ev: Event; n: VNode) =
# echo "now selected ", n.kind
setRenderer(main)

View file

@ -1,114 +0,0 @@
## Components in Karax are built by the ``.component`` macro annotation.
when defined(js):
import jdict, kdom
import macros, vdom, tables, strutils, kbase
when defined(js):
var
vcomponents* = newJDict[cstring, proc(args: seq[VNode]): VNode]()
else:
var
vcomponents* = newTable[kstring, proc(args: seq[VNode]): VNode]()
type
ComponentKind* {.pure.} = enum
None,
Tag,
VNode
var
allcomponents {.compileTime.} = initTable[string, ComponentKind]()
proc isComponent*(x: string): ComponentKind {.compileTime.} =
allcomponents.getOrDefault(x)
proc addTags() {.compileTime.} =
let x = (bindSym"VNodeKind").getTypeImpl
expectKind(x, nnkEnumTy)
for i in ord(VNodeKind.html)..ord(VNodeKind.high):
# +1 because of empty node at the start of the enum AST:
let tag = $x[i+1]
allcomponents[tag] = ComponentKind.Tag
static:
addTags()
proc unpack(symbolicType: NimNode; index: int): NimNode {.compileTime.} =
#let t = symbolicType.getTypeImpl
let t = repr(symbolicType)
case t
of "cstring":
result = quote do:
args[`index`].text
of "int", "VKey":
result = quote do:
args[`index`].intValue
of "bool":
result = quote do:
args[`index`].intValue != 0
elif t.endsWith"Kind":
result = quote do:
`symbolicType`(args[`index`].intValue)
else:
# just pass it along, maybe there is some conversion for it:
result = quote do:
args[`index`]
proc newname*(n: NimNode): NimNode =
if n.kind == nnkPostfix:
n[1] = newname(n[1])
result = n
elif n.kind == nnkSym:
result = ident(n.strVal)
else:
result = n
when defined(js):
macro compact*(prc: untyped): untyped =
## A 'compact' tree generation proc is one that only depends on its
## inputs and should be stored as a compact virtual DOM tree and
## only expanded on demand (when its inputs changed).
var n = prc.copyNimNode
for i in 0..6: n.add prc[i].copyNimTree
expectKind(n, nnkProcDef)
if n[0].kind == nnkEmpty:
error("please pass a non anonymous proc", n[0])
let name = n[0]
let params = params(n)
let rettype = repr params[0]
var isvirtual = ComponentKind.None
if rettype == "VNode":
isvirtual = ComponentKind.VNode
else:
error "component must return VNode", params[0]
let realName = if name.kind == nnkPostfix: name[1] else: name
let nn = $realName
n[0] = ident("inner" & nn)
var unpackCall = newCall(n[0])
var counter = 0
for i in 1 ..< params.len:
let param = params[i]
let L = param.len
let typ = param[L-2]
for j in 0 .. L-3:
unpackCall.add unpack(typ, counter)
inc counter
template vwrapper(pname, unpackCall) {.dirty.} =
proc pname(args: seq[VNode]): VNode =
unpackCall
template vregister(key, val) =
bind jdict.`[]=`
`[]=`(vcomponents, kstring(key), val)
result = newTree(nnkStmtList, n)
if isvirtual == ComponentKind.VNode:
result.add getAst(vwrapper(newname name, unpackCall))
result.add getAst(vregister(newLit(nn), realName))
allcomponents[nn] = isvirtual
when defined(debugKaraxDsl):
echo repr result

View file

@ -1,32 +0,0 @@
## Error handling logic for form input validation.
import karax, jdict
var
gerrorMsgs = newJDict[cstring, cstring]()
gerrorCounter = 0
proc hasErrors*(): bool = gerrorCounter != 0
proc hasError*(field: cstring): bool = gerrorMsgs.contains(field) and len(gerrorMsgs[field]) > 0
proc disableOnError*(): cstring = toDisabled(hasErrors())
proc getError*(field: cstring): cstring =
if not gerrorMsgs.contains(field):
result = ""
else:
result = gerrorMsgs[field]
proc setError*(field, msg: cstring) =
let previous = getError(field)
if len(msg) == 0:
if len(previous) != 0: dec(gerrorCounter)
else:
if len(previous) == 0: inc(gerrorCounter)
gerrorMsgs[field] = msg
proc clearErrors*() =
let m = gerrorMsgs
for k in m.keys:
setError(k, "")

View file

@ -1,152 +0,0 @@
## Karax -- Single page applications for Nim.
## i18n support for Karax applications. We distinguish between:
##
## - translate: Lookup text translations
## - localize: Localize Date and Time objects to local formats
##
## Localization has not yet been implemented.
import languages, jdict, jstrutils
var currentLanguage = Language.enUS
proc getLanguage(): cstring =
{.emit: "`result` = (navigator.languages && navigator.languages.length) ? navigator.languages[0] : navigator.language;".}
proc detectLanguage*(): Language =
let x = getLanguage()
for i in low(Language)..high(Language):
if languageToCode[i] == x: return i
return Language.enUS
proc setCurrentLanguage*(newLanguage = detectLanguage()) =
currentLanguage = newLanguage
proc getCurrentLanguage*(): Language = currentLanguage
type Translation* = JDict[cstring, cstring]
var
translations: array[Language, Translation]
proc registerTranslation*(lang: Language; t: Translation) = translations[lang] = t
proc addTranslation*(lang: Language; key, val: cstring) =
if translations[lang].isNil: translations[lang] = newJDict[cstring, cstring]()
translations[lang][key] = val
proc translate(x: cstring): cstring =
let y = translations[currentLanguage]
if y != nil and y.contains(x):
result = y[x]
else:
result = x
type TranslatedString* = distinct cstring
template i18n*(x: string): TranslatedString = TranslatedString(translate(cstring x))
proc raiseInvalidFormat(errmsg: string) =
raise newException(ValueError, errmsg)
discard """
$[1:item|items]1$ selected.
"""
proc `{}`(x: cstring; i: int): char {.importcpp: "#.charCodeAt(#)".}
proc sadd(s: JSeq[char]; c: char) {.importcpp: "#.push(String.fromCharCode(#))".}
proc parseChoice(f: cstring; i, choice: int,
r: JSeq[char]) =
var i = i
while i < f.len:
var n = 0
let oldI = i
var toAdd = false
while i < f.len and f{i} >= '0' and f{i} <= '9':
n = n * 10 + ord(f{i}) - ord('0')
inc i
if oldI != i:
if f{i} == ':':
inc i
else:
raiseInvalidFormat"':' after number expected"
toAdd = choice == n
else:
# an else section does not start with a number:
toAdd = true
while i < f.len and f{i} != ']' and f{i} != '|':
if toAdd: r.sadd f{i}
inc i
if toAdd: break
inc i
proc join(x: JSeq[char]): cstring {.importcpp: "#.join(\"\")".}
proc add(x: JSeq[char]; y: cstring) =
for i in 0..<y.len: x.sadd y{i}
proc `%`*(formatString: TranslatedString; args: openArray[cstring]): cstring =
let f = cstring(formatString)
var i = 0
var num = 0
var r = newJSeq[char]()
while i < f.len:
if f{i} == '$' and i+1 < f.len:
inc i
case f{i}
of '#':
r.add args[num]
inc i
inc num
of '1'..'9', '-':
var j = 0
var negative = f{i} == '-'
if negative: inc i
while f{i} >= '0' and f{i} <= '9':
j = j * 10 + ord(f{i}) - ord('0')
inc i
let idx = if not negative: j-1 else: args.len-j
r.add args[idx]
of '$':
inc(i)
r.add '$'
of '[':
let start = i+1
while i < f.len and f{i} != ']': inc i
inc i
if i >= f.len: raiseInvalidFormat"']' expected"
case f{i}
of '#':
parseChoice(f, start, parseInt args[num], r)
inc i
inc num
of '1'..'9', '-':
var j = 0
var negative = f{i} == '-'
if negative: inc i
while f{i} >= '0' and f{i} <= '9':
j = j * 10 + ord(f{i}) - ord('0')
inc i
let idx = if not negative: j-1 else: args.len-j
parseChoice(f, start, parseInt args[idx], r)
else: raiseInvalidFormat"argument index expected after ']'"
else:
raiseInvalidFormat("'#', '$', or number expected")
if i < f.len and f{i} == '$': inc i
else:
r.sadd f{i}
inc i
result = join(r)
when isMainModule:
addTranslation(Language.deDE, "$#$ -> $#$", "$2 macht $1")
setCurrentLanguage(Language.deDE)
echo(i18n"$#$ -> $#$" % [cstring"1", "2"])
echo(i18n"$[1:item |items ]1$ -> $1" % [cstring"1", "2"])
echo(i18n"$[1:item |items ]1 -> $1" % [cstring"0", "2"])
echo(i18n"$[1:item |items ]1 -> $1" % [cstring"2", "2"])
echo(i18n"$[1:item |items ]1$ -> $1" % [cstring"3", "2"])
echo(i18n"$1 $[1:item |4:huha |items ]1$ -> $1" % [cstring"4", "2"])

View file

@ -1,30 +0,0 @@
type
JDict*[K, V] = ref object
proc `[]`*[K, V](d: JDict[K, V], k: K): V {.importcpp: "#[#]".}
proc `[]=`*[K, V](d: JDict[K, V], k: K, v: V) {.importcpp: "#[#] = #".}
proc newJDict*[K, V](): JDict[K, V] {.importcpp: "{@}".}
proc contains*[K, V](d: JDict[K, V], k: K): bool {.importcpp: "#.hasOwnProperty(#)".}
proc del*[K, V](d: JDict[K, V], k: K) {.importcpp: "delete #[#]".}
iterator keys*[K, V](d: JDict[K, V]): K =
var kkk: K
{.emit: ["for (", kkk, " in ", d, ") {"].}
yield kkk
{.emit: ["}"].}
type
JSeq*[T] = ref object
proc `[]`*[T](s: JSeq[T], i: int): T {.importcpp: "#[#]", noSideEffect.}
proc `[]=`*[T](s: JSeq[T], i: int, v: T) {.importcpp: "#[#] = #", noSideEffect.}
proc newJSeq*[T](len: int = 0): JSeq[T] {.importcpp: "new Array(#)".}
proc len*[T](s: JSeq[T]): int {.importcpp: "#.length", noSideEffect.}
proc add*[T](s: JSeq[T]; x: T) {.importcpp: "#.push(#)", noSideEffect.}
proc shrink*[T](s: JSeq[T]; shorterLen: int) {.importcpp: "#.length = #", noSideEffect.}

View file

@ -1,67 +0,0 @@
## This module implements some small zero-overhead 'JsonNode' type
## and helper that maps directly to JavaScript objects.
type
JsonNode* {.importc.} = ref object
proc `[]`*(obj: JsonNode; fieldname: cstring): JsonNode {.importcpp: "#[#]".}
proc `[]`*(obj: JsonNode; index: int): JsonNode {.importcpp: "#[#]".}
proc `[]=`*[T](obj: JsonNode; fieldname: cstring; value: T)
{.importcpp: "#[#] = #".}
proc length(x: JsonNode): int {.importcpp: "#.length".}
proc len*(x: JsonNode): int = (if x.isNil: 0 else: x.length)
proc newJsonNode*(fields: varargs[(cstring, JsonNode)]): JsonNode =
result = JsonNode()
for f in fields:
result[f[0]] = f[1]
proc newJObject*(): JsonNode =
result = JsonNode()
proc newJArray*(elements: varargs[JsonNode]): JsonNode {.importcpp: "#".}
proc newJNull*(): JsonNode = nil
template `%`*(x: typed): JsonNode = cast[JsonNode](x)
template `%`*(x: string): JsonNode = cast[JsonNode](cstring x)
proc getNum*(x: JsonNode): int {.importcpp: "#".}
proc getInt*(x: JsonNode): int {.importcpp: "#".}
proc getStr*(x: JsonNode): cstring {.importcpp: "#".}
proc getFNum*(x: JsonNode): cstring {.importcpp: "#".}
iterator items*(x: JsonNode): JsonNode =
for i in 0..<len(x): yield x[i]
import macros
proc toJson(x: NimNode): NimNode {.compiletime.} =
case x.kind
of nnkBracket:
result = newCall(bindSym"newJArray")
for i in 0 ..< x.len:
result.add(toJson(x[i]))
of nnkTableConstr:
result = newCall(bindSym"newJsonNode")
for i in 0 ..< x.len:
x[i].expectKind nnkExprColonExpr
let key = x[i][0]
let a = if x[i].kind in {nnkIdent, nnkSym, nnkAccQuoted}:
newLit($key)
else:
key
result.add newTree(nnkPar, newCall(bindSym"cstring", a), toJson(x[i][1]))
of nnkCurly:
x.expectLen(0)
result = newCall(bindSym"newJObject")
of nnkNilLit:
result = newCall(bindSym"newJNull")
else:
result = newCall(bindSym"%", x)
macro `%*`*(x: untyped): untyped =
## Convert an expression to a JsonNode directly, without having to specify
## `%` for every element.
result = toJson(x)

View file

@ -1,17 +0,0 @@
## Websockets support for the JSON backend.
type
MessageEvent* {.importc.} = ref object
data*: cstring
WebSocket* {.importc.} = ref object
onmessage*: proc (e: MessageEvent)
onopen*: proc (e: MessageEvent)
proc newWebSocket*(url, key: cstring): WebSocket
{.importcpp: "new WebSocket(@)".}
proc newWebSocket*(url: cstring): WebSocket
{.importcpp: "new WebSocket(@)".}
proc send*(w: WebSocket; data: cstring) {.importcpp.}

View file

@ -1,132 +0,0 @@
## Karax -- Single page applications for Nim.
## This module implements support for `ajax`:idx: socket
## handling.
import karax
import jsffi except `&`
import jscore
import dom
type
ProgressEvent* {.importc.}= object
loaded*: float
total*: float
FormData* {.importc.} = JsObject
HttpRequest* {.importc.} = ref object
ThisObj {.importc.} = ref object
readyState, status: int
responseText, statusText: cstring
XMLHttpRequestUpload* {.importc.} = JsObject
proc newFormData*(): FormData {.importcpp: "new FormData()".}
proc append*(f: FormData, key: cstring, value: Blob) {.importcpp:"#.append(@)".}
proc append*(f: FormData, key: cstring, value: cstring) {.importcpp:"#.append(@)".}
proc setRequestHeader*(r: HttpRequest; a, b: cstring) {.importcpp: "#.setRequestHeader(@)".}
proc statechange*(r: HttpRequest; cb: proc()) {.importcpp: "#.onreadystatechange = #".}
proc send*(r: HttpRequest; data: cstring) {.importcpp: "#.send(#)".}
proc send*(r: HttpRequest, data: Blob) {.importcpp: "#.send(#)".}
proc open*(r: HttpRequest; meth, url: cstring; async: bool) {.importcpp: "#.open(@)".}
proc newRequest*(): HttpRequest {.importcpp: "new XMLHttpRequest(@)".}
when not declared(dom.File):
type
DomFile = ref FileObj
FileObj {.importc.} = object of Blob
lastModified: int
name: cstring
else:
type
DomFile = dom.File
proc uploadFile*(url: cstring, file: Blob, onprogress :proc(data: ProgressEvent),
cont: proc (httpStatus: int; response: cstring);
headers: openarray[(cstring, cstring)] = []) =
proc contWrapper(httpStatus: int; response: cstring) =
cont(httpStatus, response)
proc upload(r: HttpRequest):XMLHttpRequestUpload {.importcpp: "#.upload".}
var this {.importc: "this".}: ThisObj
var formData = newFormData()
formData.append("upload_file",file)
formData.append("filename", DomFile(file).name)
let ajax = newRequest()
ajax.open("POST", url, true)
for a, b in items(headers):
ajax.setRequestHeader(a, b)
ajax.statechange proc() =
if this.readyState == 4:
contWrapper(this.status, this.responseText)
ajax.upload.onprogress = onprogress
ajax.send(formData.to(cstring))
proc ajax*(meth, url: cstring; headers: openarray[(cstring, cstring)];
data: cstring;
cont: proc (httpStatus: int; response: cstring);
doRedraw: bool = true,
kxi: KaraxInstance = kxi,
useBinary: bool = false,
blob: Blob = nil) =
proc contWrapper(httpStatus: int; response: cstring) =
cont(httpStatus, response)
if doRedraw: redraw(kxi)
var this {.importc: "this".}: ThisObj
let ajax = newRequest()
ajax.open(meth, url, true)
for a, b in items(headers):
ajax.setRequestHeader(a, b)
ajax.statechange proc() =
if this.readyState == 4:
if this.status == 200:
contWrapper(this.status, this.responseText)
else:
contWrapper(this.status, this.responseText)
if useBinary:
ajax.send(blob)
else:
ajax.send(data)
proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)];
data: cstring;
cont: proc (httpStatus: int, response: cstring);
doRedraw: bool = true,
kxi: KaraxInstance = kxi) =
ajax("POST", url, headers, data, cont, doRedraw, kxi)
proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)];
data: Blob;
cont: proc (httpStatus: int, response: cstring);
doRedraw: bool = true,
kxi: KaraxInstance = kxi) =
ajax("POST", url, headers, "", cont, doRedraw, kxi, true, data)
proc ajaxGet*(url: cstring; headers: openarray[(cstring, cstring)];
cont: proc (httpStatus: int, response: cstring);
doRedraw: bool = true,
kxi: KaraxInstance = kxi) =
ajax("GET", url, headers, nil, cont, doRedraw, kxi)
proc ajaxPut*(url: cstring; headers: openarray[(cstring, cstring)];
data: cstring;
cont: proc (httpStatus: int, response: cstring);
doRedraw: bool = true,
kxi: KaraxInstance = kxi) =
ajax("PUT", url, headers, data, cont, doRedraw, kxi)
proc ajaxDelete*(url: cstring; headers: openarray[(cstring, cstring)];
cont: proc (httpStatus: int, response: cstring);
doRedraw: bool = true,
kxi: KaraxInstance = kxi) =
ajax("DELETE", url, headers, nil, cont, doRedraw, kxi)
proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".}
proc fromJson*[T](blob: cstring): T {.importc: "JSON.parse".}

View file

@ -1,834 +0,0 @@
## Karax -- Single page applications for Nim.
import kdom, vdom, jstrutils, compact, jdict, vstyles
export kdom.Event, kdom.Blob
when defined(nimNoNil):
{.experimental: "notnil".}
proc kout*[T](x: T) {.importc: "console.log", varargs.}
## The preferred way of debugging karax applications.
type
PatchKind = enum
pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach, pkSame
Patch = object
k: PatchKind
parent, current: Node
newNode, oldNode: VNode
PatchV = object
parent, newChild: VNode
pos: int
ComponentPair = object
oldNode, newNode: VComponent
parent, current: Node
type
RouterData* = ref object ## information that is passed to the 'renderer' callback
hashPart*: cstring ## the hash part of the URL for routing.
KaraxInstance* = ref object ## underlying karax instance. Usually you don't have
## know about this.
rootId: cstring
renderer: proc (data: RouterData): VNode {.closure.}
currentTree: VNode
postRenderCallback: proc (data: RouterData)
toFocus: Node
toFocusV: VNode
renderId: int
rendering: bool
patches: seq[Patch] # we reuse this to save allocations
patchLen: int
patchesV: seq[PatchV]
patchLenV: int
runCount: int
components: seq[ComponentPair]
surpressRedraws*: bool
byId: JDict[cstring, VNode]
when defined(stats):
recursion: int
orphans: JDict[cstring, bool]
var
kxi*: KaraxInstance ## The current Karax instance. This is always used
## as the default. **Note**: Within the karax DSL
## always a symbol of the name *kxi* is assumed, so
## if you have a local karax instance to use instead
## in your 'buildHtml' statement, it needs to be named
## 'kxi'.
proc setFocus*(n: VNode; enabled = true; kxi: KaraxInstance = kxi) =
if enabled:
kxi.toFocusV = n
# ----------------- event wrapping ---------------------------------------
template nativeValue(ev): cstring = cast[Element](ev.target).value
template setNativeValue(ev, val) = cast[Element](ev.target).value = val
template keyeventBody() =
let v = nativeValue(ev)
n.value = v
assert action != nil
action(ev, n)
if n.value != v:
setNativeValue(ev, n.value)
# Do not call redraw() here! That is already done
# by ``karax.addEventHandler``.
proc karaxEvents(d: Node): JSeq[(cstring, NativeEventHandler)] {.importcpp: "#.karaxEvents".}
proc `karaxEvents=`(d: Node; value: JSeq[(cstring, NativeEventHandler)]) {.importcpp: "#.karaxEvents = #".}
proc addEventShell(d: Node; name: cstring; h: NativeEventHandler) =
# The DOM is such a pathetic piece of junk that it doesn't
# offer 'removeAllEventHandlers()'. Hence we store the event
# handler twice in 'd' so that we can emulate this properly.
# This is required to fix bug #139.
d.addEventListener(name, h)
if d.karaxEvents == nil:
d.karaxEvents = newJSeq[(cstring, NativeEventHandler)]()
d.karaxEvents.add((name, h))
proc removeAllEventHandlers(d: Node) =
if d.karaxEvents != nil:
for i in 0..<d.karaxEvents.len:
d.removeEventListener(d.karaxEvents[i][0], d.karaxEvents[i][1])
proc wrapEvent(d: Node; n: VNode; k: EventKind;
action: EventHandler): NativeEventHandler =
proc stdWrapper(): NativeEventHandler =
let action = action
let n = n
result = proc (ev: Event) =
if n.kind == VNodeKind.textarea or n.kind == VNodeKind.input or n.kind == VNodeKind.select:
keyeventBody()
else: action(ev, n)
proc enterWrapper(): NativeEventHandler =
let action = action
let n = n
result = proc (ev: Event) =
if cast[KeyboardEvent](ev).keyCode == 13: keyeventBody()
proc laterWrapper(): NativeEventHandler =
let action = action
let n = n
var timer: Timeout
result = proc (ev: Event) =
proc wrapper() = keyeventBody()
if timer != nil: clearTimeout(timer)
timer = setTimeout(wrapper, 400)
case k
of EventKind.onkeyuplater:
result = laterWrapper()
d.addEventShell("keyup", result)
of EventKind.onkeyupenter:
result = enterWrapper()
d.addEventShell("keyup", result)
else:
result = stdWrapper()
d.addEventShell(toEventName[k], result)
# --------------------- DOM diff -----------------------------------------
template detach(n: VNode) =
addPatch(kxi, pkDetach, nil, nil, nil, n)
template attach(n: VNode) =
n.dom = result
if n.id != nil: kxi.byId[n.id] = n
proc applyEvents(n: VNode) =
let dest = n.dom
for i in 0..<len(n.events):
n.events[i][2] = wrapEvent(dest, n, n.events[i][0], n.events[i][1])
proc getVNodeById*(id: cstring; kxi: KaraxInstance = kxi): VNode =
## Get the VNode that was marked with ``id``. Returns ``nil``
## if no node exists.
if kxi.byId.contains(id):
result = kxi.byId[id]
proc toDom*(n: VNode; useAttachedNode: bool; kxi: KaraxInstance = nil): Node =
if useAttachedNode:
if n.dom != nil:
if n.id != nil: kxi.byId[n.id] = n
return n.dom
if n.kind == VNodeKind.text:
result = document.createTextNode(n.text)
attach n
elif n.kind == VNodeKind.verbatim:
result = document.createElement("div")
result.innerHTML = n.text
attach n
return result
elif n.kind == VNodeKind.vthunk:
let x = callThunk(vcomponents[n.text], n)
result = toDom(x, useAttachedNode, kxi)
#n.key = result.key
attach n
return result
elif n.kind == VNodeKind.dthunk:
result = n.dom #callThunk(dcomponents[n.text], n)
assert result != nil
#n.key = result.key
attach n
return result
elif n.kind == VNodeKind.component:
let x = VComponent(n)
if x.onAttachImpl != nil: x.onAttachImpl(x)
assert x.renderImpl != nil
if x.expanded == nil:
x.expanded = x.renderImpl(x)
# x.updatedImpl(x, nil)
assert x.expanded != nil
result = toDom(x.expanded, useAttachedNode, kxi)
attach n
return result
else:
result = document.createElement(toTag[n.kind])
attach n
for k in n:
appendChild(result, toDom(k, useAttachedNode, kxi))
# text is mapped to 'value':
if n.text != nil:
result.value = n.text
if n.id != nil:
result.id = n.id
if n.class != nil:
result.class = n.class
#if n.key >= 0:
# result.key = n.key
for k, v in attrs(n):
if v != nil:
result.setAttr(k, v)
applyEvents(n)
if kxi != nil and n == kxi.toFocusV and kxi.toFocus.isNil:
kxi.toFocus = result
if not n.style.isNil: applyStyle(result, n.style)
proc same(n: VNode, e: Node; nesting = 0): bool =
if kxi.orphans.contains(n.id): return true
if n.kind == VNodeKind.component:
result = same(VComponent(n).expanded, e, nesting+1)
elif n.kind == VNodeKind.verbatim:
result = true
elif n.kind == VNodeKind.vthunk or n.kind == VNodeKind.dthunk:
# we don't check these:
result = true
elif toTag[n.kind] == e.nodename:
result = true
if n.kind != VNodeKind.text:
# BUGFIX: Microsoft's Edge gives the textarea a child containing the text node!
if e.len != n.len and n.kind != VNodeKind.textarea:
echo "expected ", n.len, " real ", e.len, " ", toTag[n.kind], " nesting ", nesting
return false
for i in 0 ..< n.len:
if not same(n[i], e[i], nesting+1): return false
else:
echo "VDOM: ", toTag[n.kind], " DOM: ", e.nodename
proc replaceById(id: cstring; newTree: Node) =
let x = document.getElementById(id)
x.parentNode.replaceChild(newTree, x)
newTree.id = id
type
EqResult = enum
componentsIdentical, different, similar, identical, usenewNode
when defined(profileKarax):
type
DifferEnum = enum
deKind, deId, deIndex, deText, deComponent, deClass,
deSimilar
var
reasons: array[DifferEnum, int]
proc echa(a: array[DifferEnum, int]) =
for i in low(DifferEnum)..high(DifferEnum):
echo i, " value: ", a[i]
proc eq(a, b: VNode; recursive: bool): EqResult =
if a.kind != b.kind:
when defined(profileKarax): inc reasons[deKind]
return different
if a.id != b.id:
when defined(profileKarax): inc reasons[deId]
return different
result = identical
if a.index != b.index:
when defined(profileKarax): inc reasons[deIndex]
return different
if a.kind == VNodeKind.text:
if a.text != b.text:
when defined(profileKarax): inc reasons[deText]
return different # similar
elif a.kind == VNodeKind.vthunk:
if a.text != b.text: return different
if a.len != b.len: return different
for i in 0..<a.len:
if eq(a[i], b[i], recursive) == different: return different
elif a.kind == VNodeKind.dthunk:
if a.dom == b.dom:
return identical
else: # fix #119
return different
elif a.kind == VNodeKind.verbatim:
if a.text != b.text:
return different
elif b.kind == VNodeKind.component:
# different component names mean different components:
if a.text != b.text:
when defined(profileKarax): inc reasons[deComponent]
return different
#if VComponent(a).key.isNil and VComponent(b).key.isNil:
# when defined(profileKarax): inc reasons[deComponent]
# return different
if VComponent(a).key != VComponent(b).key:
when defined(profileKarax): inc reasons[deComponent]
return different
return componentsIdentical
#if:
# when defined(profileKarax): inc reasons[deClass]
# return different
if a.class != b.class or not eq(a.style, b.style) or not sameAttrs(a, b):
when defined(profileKarax): inc reasons[deSimilar]
return similar
if recursive:
if a.len != b.len:
return different
for i in 0..<a.len:
if eq(a[i], b[i], true) != identical:
return different
# Do not test event listeners here!
return result
proc updateStyles(newNode, oldNode: VNode) =
# we keep the oldNode, but take over the style from the new node:
if oldNode.dom != nil:
if newNode.style != nil: applyStyle(oldNode.dom, newNode.style)
else: oldNode.dom.style = Style()
oldNode.dom.class = newNode.class
oldNode.style = newNode.style
oldNode.class = newNode.class
proc updateAttributes(newNode, oldNode: VNode) =
# we keep the oldNode, but take over the attributes from the new node:
if oldNode.dom != nil:
for k, _ in attrs(oldNode):
oldNode.dom.removeAttribute(k)
for k, v in attrs(newNode):
if v != nil:
oldNode.dom.setAttr(k, v)
takeOverAttr(newNode, oldNode)
proc mergeEvents(newNode, oldNode: VNode; kxi: KaraxInstance) =
let d = oldNode.dom
if d != nil:
removeAllEventHandlers(d)
when false:
for i in 0..<oldNode.events.len:
let k = oldNode.events[i][0]
let name = case k
of EventKind.onkeyuplater, EventKind.onkeyupenter: cstring"keyup"
else: toEventName[k]
d.removeEventListener(name, oldNode.events[i][2])
shallowCopy(oldNode.events, newNode.events)
applyEvents(oldNode)
when false:
proc printV(n: VNode; depth: cstring = "") =
kout depth, cstring($n.kind), cstring"key ", n.index
#for k, v in pairs(n.style):
# kout depth, "style: ", k, v
if n.kind == VNodeKind.component:
let nn = VComponent(n)
if nn.expanded != nil: printV(nn.expanded, ">>" & depth)
elif n.kind == VNodeKind.text:
kout depth, n.text
for i in 0 ..< n.len:
printV(n[i], depth & " ")
proc addPatch(kxi: KaraxInstance; ka: PatchKind; parenta, currenta: Node;
na, oldNode: VNode) =
let L = kxi.patchLen
if L >= kxi.patches.len:
# allocate more space:
kxi.patches.add(Patch(k: ka, parent: parenta, current: currenta,
newNode: na, oldNode: oldNode))
else:
kxi.patches[L].k = ka
kxi.patches[L].parent = parenta
kxi.patches[L].current = currenta
kxi.patches[L].newNode = na
kxi.patches[L].oldNode = oldNode
inc kxi.patchLen
proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) =
let L = kxi.patchLenV
if L >= kxi.patchesV.len:
# allocate more space:
kxi.patchesV.add(PatchV(parent: parent, newChild: newChild, pos: pos))
else:
kxi.patchesV[L].parent = parent
kxi.patchesV[L].newChild = newChild
kxi.patchesV[L].pos = pos
inc kxi.patchLenV
proc moveDom(dest, src: VNode) =
dest.dom = src.dom
src.dom = nil
if dest.id != nil:
kxi.byId[dest.id] = dest
assert dest.len == src.len
for i in 0..<dest.len:
moveDom(dest[i], src[i])
proc applyPatch(kxi: KaraxInstance) =
for i in 0..<kxi.patchLen:
let p = kxi.patches[i]
case p.k
of pkReplace:
let nn = toDom(p.newNode, useAttachedNode = true, kxi)
if p.parent == nil:
replaceById(kxi.rootId, nn)
else:
if p.current.parentNode == p.parent:
p.parent.replaceChild(nn, p.current)
else: # fix #121
p.parent.appendChild(nn)
of pkSame:
moveDom(p.newNode, p.oldNode)
of pkRemove:
p.parent.removeChild(p.current)
of pkAppend:
let nn = toDom(p.newNode, useAttachedNode = true, kxi)
p.parent.appendChild(nn)
of pkInsertBefore:
let nn = toDom(p.newNode, useAttachedNode = true, kxi)
p.parent.insertBefore(nn, p.current)
of pkDetach:
let n = p.oldNode
if n.id != nil: kxi.byId.del(n.id)
if n.kind == VNodeKind.component:
let x = VComponent(n)
if x.onDetachImpl != nil: x.onDetachImpl(x)
# XXX for some reason this causes assertion errors otherwise:
if not kxi.surpressRedraws: n.dom = nil
kxi.patchLen = 0
for i in 0..<kxi.patchLenV:
let p = kxi.patchesV[i]
p.parent[p.pos] = p.newChild
assert p.newChild.dom != nil
kxi.patchLenV = 0
# ASSUME: We patch both the virtual DOM and the real DOM and throw away
# the newly produced DOM. Thus on updates like 'newNode.dom = oldNode.dom'
# are required. The only exception is when the top level node is replaced.
# Then we have to take the new virtual DOM. In fact, we trigger a full DOM
# rebuild then. However, we don't have to consider old event handlers then
# so everything stays simple.
# We also do not produce "Patch sets" anymore, everything is done as simply
# as possible. Ok, let's assume that we seek to update event handler lists:
# The new node has captures to itself or to other new nodes, never to old
# nodes! --> We cannot ever use the old VDOM, we have to use the new virtual
# DOM. For identical nodes we need to take over the .dom field from the old
# node since we don't recompute them. This must be done recursively. In
# vnodeToDom we have to check whether the 'dom' field was already set. If so,
# There is nothing to do.
# "Similar" nodes can have the opposite effect; consider
#
# AAAABAAAA
# AAAACDAAAA
#
# In this example B did change to C and 'D' is new. However, replacing B by
# CD is fine.
#
proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance) =
when defined(stats):
if kxi.recursion > 100:
echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode, false)
if oldNode.kind == VNodeKind.text:
echo oldNode.text
inc kxi.recursion
let result = eq(newNode, oldNode, false)
case result
of componentsIdentical:
kxi.components.add ComponentPair(oldNode: VComponent(oldNode),
newNode: VComponent(newNode),
parent: parent,
current: current)
of identical, similar:
newNode.dom = oldNode.dom
if result == similar:
updateStyles(newNode, oldNode)
updateAttributes(newNode, oldNode)
if oldNode.kind == VNodeKind.text:
oldNode.text = newNode.text
oldNode.dom.nodeValue = newNode.text
if newNode.events.len != 0 or oldNode.events.len != 0:
mergeEvents(newNode, oldNode, kxi)
let newLength = newNode.len
let oldLength = oldNode.len
if newLength == 0 and oldLength == 0: return
let minLength = min(newLength, oldLength)
assert oldNode.kind == newNode.kind
var commonPrefix = 0
# compute common prefix:
while commonPrefix < minLength:
if eq(newNode[commonPrefix], oldNode[commonPrefix], true) == identical:
kxi.addPatch(pkSame, nil, nil, newNode[commonPrefix], oldNode[commonPrefix])
inc commonPrefix
else:
break
# compute common suffix:
var oldPos = oldLength - 1
var newPos = newLength - 1
while oldPos >= commonPrefix and newPos >= commonPrefix:
if eq(newNode[newPos], oldNode[oldPos], true) == identical:
kxi.addPatch(pkSame, nil, nil, newNode[newPos], oldNode[oldPos])
dec oldPos
dec newPos
else:
break
let pos = min(oldPos, newPos) + 1
# now the different children are in commonPrefix .. pos - 1:
for i in commonPrefix..pos-1:
diff(newNode[i], oldNode[i], current, current.childNodes[i], kxi)
if oldPos + 1 == oldLength:
for i in pos..newPos:
kxi.addPatch(pkAppend, current, nil, newNode[i], nil)
else:
let before = current.childNodes[oldPos + 1]
for i in pos..newPos:
kxi.addPatch(pkInsertBefore, current, before, newNode[i], nil)
# XXX call 'attach' here?
for i in pos..oldPos:
detach(oldNode[i])
#doAssert i < current.childNodes.len
kxi.addPatch(pkRemove, current, current.childNodes[i], nil, nil)
of different:
detach(oldNode)
kxi.addPatch(pkReplace, parent, current, newNode, nil)
of usenewNode: doAssert(false, "eq returned usenewNode")
when defined(stats):
dec kxi.recursion
proc applyComponents(kxi: KaraxInstance) =
# the first 'diff' pass detects components in the VDOM. The
# 'applyComponents' expands components and so on until no
# components are left to check.
var i = 0
# beware: 'diff' appends to kxi.components!
# So this is actually a fixpoint iteration:
while i < kxi.components.len:
let x = kxi.components[i].oldNode
let newNode = kxi.components[i].newNode
when defined(karaxDebug):
echo "Processing component ", newNode.text, " changed impl set ", x.changedImpl != nil
if x.changedImpl != nil and x.changedImpl(x, newNode):
when defined(karaxDebug):
echo "Component ", newNode.text, " did change"
let current = kxi.components[i].current
let parent = kxi.components[i].parent
x.updatedImpl(x, newNode)
let oldExpanded = x.expanded
x.expanded = x.renderImpl(x)
when defined(karaxDebug):
echo "Component ", newNode.text, " re-rendered"
x.renderedVersion = x.version
if oldExpanded == nil:
detach(x)
kxi.addPatch(pkReplace, parent, current, x.expanded, nil)
when defined(karaxDebug):
echo "Component ", newNode.text, ": old expansion didn't exist"
else:
diff(x.expanded, oldExpanded, parent, current, kxi)
when false:
if res == usenewNode:
when defined(karaxDebug):
echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)"
discard "diff created a patchset for us, so this is fine"
elif res != different:
when defined(karaxDebug):
echo "Component ", newNode.text, ": re-render triggered no DOM change whatsoever"
x.expanded = oldExpanded
assert oldExpanded.dom != nil, "old expanded.dom is nil"
else:
when defined(karaxDebug):
echo "Component ", newNode.text, ": re-render triggered a DOM change (case B)"
assert x.expanded.dom != nil, "expanded.dom is nil"
inc i
setLen(kxi.components, 0)
when defined(stats):
proc depth(n: VNode; total: var int): int =
var m = 0
for i in 0..<n.len:
m = max(m, depth(n[i], total))
result = m + 1
inc total
proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) =
detach(parent[position])
let current = parent.dom
kxi.addPatch(pkRemove, current, current.childNodes[position], nil, nil)
parent.delete(position)
applyPatch(kxi)
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) =
let current = parent.dom
if position >= parent.len:
kxi.addPatch(pkAppend, current, nil, kid, nil)
parent.add(kid)
else:
let before = current.childNodes[position]
kxi.addPatch(pkInsertBefore, current, before, kid, nil)
parent.insert(kid, position)
applyPatch(kxi)
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) =
let olddom = oldNode.dom
doAssert olddom != nil
diff(newNode, oldNode, nil, olddom, kxi)
# this is a bit nasty: Since we cannot patch the 'parent' of
# the current VNode (because we don't store it at all!), we
# need to override the fields individually:
takeOverFields(newNode, oldNode)
applyComponents(kxi)
applyPatch(kxi)
if kxi.currentTree == oldNode:
kxi.currentTree = newNode
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
var onhashChange {.importc: "window.onhashchange".}: proc()
var hashPart {.importc: "window.location.hash".}: cstring
proc avoidDomDiffing*(kxi: KaraxInstance = kxi) =
## enforce a full redraw for the next redraw operation.
## This can be used as a temporary way to workaround DOM diffing
## problems or to avoid the DOM diffing when you already know
## it should use a completely new DOM.
## This is an experimental API.
kxi.currentTree = nil
proc reqFrame(callback: proc()): int {.importc: "window.requestAnimationFrame".}
when false:
proc cancelFrame(id: int) {.importc: "window.cancelAnimationFrame".}
proc dodraw(kxi: KaraxInstance) =
if kxi.renderer.isNil: return
kxi.renderId = 0
if kxi.rendering:
# there is a render already running, delay 1 frame
kxi.renderId = reqFrame(proc () = kxi.dodraw)
return
kxi.rendering = true
let rdata = RouterData(hashPart: hashPart)
let newtree = kxi.renderer(rdata)
inc kxi.runCount
newtree.id = kxi.rootId
kxi.toFocus = nil
if kxi.currentTree == nil:
let asdom = toDom(newtree, useAttachedNode = true, kxi)
replaceById(kxi.rootId, asdom)
else:
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
let olddom = document.getElementById(kxi.rootId)
diff(newtree, kxi.currentTree, nil, olddom, kxi)
when defined(profileKarax):
echo "<<<<<<<<<<<<<<"
echa reasons
applyComponents(kxi)
when defined(profileKarax):
echo "--------------"
echa reasons
echo ">>>>>>>>>>>>>>"
applyPatch(kxi)
kxi.currentTree = newtree
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
if not kxi.postRenderCallback.isNil:
kxi.postRenderCallback(rdata)
# now that it's part of the DOM, give it the focus:
if kxi.toFocus != nil:
kxi.toFocus.focus()
kxi.rendering = false
when defined(stats):
kxi.recursion = 0
var total = 0
echo "depth ", depth(kxi.currentTree, total), " total ", total
proc redraw*(kxi: KaraxInstance = kxi) =
# we buffer redraw requests:
when false:
if drawTimeout != nil:
clearTimeout(drawTimeout)
drawTimeout = setTimeout(dodraw, 30)
elif true:
if kxi.renderId == 0:
kxi.renderId = reqFrame(proc () = kxi.dodraw)
else:
dodraw(kxi)
proc redrawSync*(kxi: KaraxInstance = kxi) = dodraw(kxi)
proc init(ev: Event) =
kxi.renderId = reqFrame(proc () = kxi.dodraw)
proc setRenderer*(renderer: proc (data: RouterData): VNode,
root: cstring = "ROOT",
clientPostRenderCallback:
proc (data: RouterData) = nil): KaraxInstance {.
discardable.} =
## Setup Karax. Usually the return value can be ignored.
if document.getElementById(root).isNil:
let msg = "Could not find a <div> with id=" & root &
". Karax needs it as its rendering target."
raise newException(Exception, $msg)
result = KaraxInstance(rootId: root, renderer: renderer,
postRenderCallback: clientPostRenderCallback,
patches: newSeq[Patch](60),
patchesV: newSeq[PatchV](30),
components: @[],
surpressRedraws: false,
byId: newJDict[cstring, VNode](),
orphans: newJDict[cstring, bool]())
kxi = result
window.onload = init
onhashChange = proc() = redraw()
proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT",
clientPostRenderCallback: proc () = nil): KaraxInstance {.discardable.} =
## Setup Karax. Usually the return value can be ignored.
proc wrapRenderer(data: RouterData): VNode = result = renderer()
proc wrapPostRender(data: RouterData) =
if clientPostRenderCallback != nil: clientPostRenderCallback()
setRenderer(wrapRenderer, root, wrapPostRender)
proc setInitializer*(renderer: proc (data: RouterData): VNode, root: cstring = "ROOT",
clientPostRenderCallback:
proc (data: RouterData) = nil): KaraxInstance {.discardable.} =
## Setup Karax. Usually the return value can be ignored.
result = KaraxInstance(rootId: root, renderer: renderer,
postRenderCallback: clientPostRenderCallback,
patches: newSeq[Patch](60),
patchesV: newSeq[PatchV](30),
components: @[],
surpressRedraws: true,
byId: newJDict[cstring, VNode](),
orphans: newJDict[cstring, bool]())
kxi = result
window.onload = init
proc addEventHandler*(n: VNode; k: EventKind; action: EventHandler;
kxi: KaraxInstance = kxi) =
## Implements the foundation of Karax's event management.
## Karax DSL transforms ``tag(onEvent = handler)`` to
## ``tempNode.addEventHandler(tagNode, EventKind.onEvent, wrapper)``
## where ``wrapper`` calls the passed ``action`` and then triggers
## a ``redraw``.
proc wrapper(ev: Event; n: VNode) =
action(ev, n)
if not kxi.surpressRedraws: redraw(kxi)
addEventListener(n, k, wrapper)
proc addEventHandler*(n: VNode; k: EventKind; action: proc();
kxi: KaraxInstance = kxi) =
## Implements the foundation of Karax's event management.
## Karax DSL transforms ``tag(onEvent = handler)`` to
## ``tempNode.addEventHandler(tagNode, EventKind.onEvent, wrapper)``
## where ``wrapper`` calls the passed ``action`` and then triggers
## a ``redraw``.
proc wrapper(ev: Event; n: VNode) =
action()
if not kxi.surpressRedraws: redraw(kxi)
addEventListener(n, k, wrapper)
proc setOnHashChange*(action: proc (hashPart: cstring)) {.deprecated: "use setRenderer instead".} =
## Now deprecated, instead pass a callback to ``setRenderer`` that receives
## a ``data: RouterData`` parameter.
proc wrapper() =
action(hashPart)
redraw()
onhashchange = wrapper
proc setForeignNodeId*(id: cstring; kxi: KaraxInstance = kxi) =
## Declares a node ID as "foreign". Foreign nodes are not
## under Karax's control in the sense that Karax does not attempt
## to perform structural checks on them.
kxi.orphans[id] = true
{.push stackTrace:off.}
proc setupErrorHandler*() =
## Installs an error handler that transforms native JS unhandled
## exceptions into Nim based stack traces. If `useAlert` is false,
## the error message is put into the console, otherwise `alert`
## is called.
proc stackTraceAsCstring(): cstring = cstring(getStackTrace())
var onerror {.importc: "window.onerror", used.} =
proc (msg, url: cstring, line, col: int, error: cstring): bool =
var x = cstring"Error: " & msg & "\n" & stackTraceAsCstring()
echo(x)
return true # suppressErrorAlert
{.pop.}
proc prepend(parent, kid: Element) =
parent.insertBefore(kid, parent.firstChild)
proc loadScript*(jsfilename: cstring; kxi: KaraxInstance = kxi) =
let s = document.createElement("script")
s.setAttr "type", "text/javascript"
s.setAttr "src", jsfilename
document.body.prepend(s)
redraw(kxi)
proc runLater*(action: proc(); later = 400): Timeout {.discardable.} =
proc wrapper() =
action()
redraw()
result = setTimeout(wrapper, later)
proc setInputText*(n: VNode; s: cstring) =
## Sets the text of input elements.
n.text = s
if n.dom != nil: n.dom.value = s
proc getInputText*(n: VNode): cstring =
if n.dom != nil:
result = n.dom.value
proc toChecked*(checked: bool): cstring =
(if checked: cstring"checked" else: cstring(nil))
proc toDisabled*(disabled: bool): cstring =
(if disabled: cstring"disabled" else: cstring(nil))
proc toSelected*(selected: bool): cstring =
(if selected: cstring"selected" else: cstring(nil))
proc vnodeToDom*(n: VNode; kxi: KaraxInstance = nil): Node =
result = toDom(n, useAttachedNode = false, kxi)

View file

@ -1,8 +0,0 @@
## Karax -- Single page applications for Nim.
## Basic type definitions for Karax.
when defined(js):
type kstring* = cstring
else:
type kstring* = string

View file

@ -1,10 +0,0 @@
## Karax -- Single page applications for Nim.
from dom import nil
when not declared(dom.DomApiVersion):
include kdom_impl
else:
import dom
export dom

File diff suppressed because it is too large Load diff

View file

@ -1,616 +0,0 @@
## Karax -- Single page applications for Nim.
type
Language* {.pure.} = enum ## A pure enum that contains most (all?)
## known human languages in current use.
afZA ## "South Africa - Afrikaans"
amET ## "Ethiopia - Amharic"
arAE ## "U.A.E. - Arabic"
arBH ## "Bahrain - Arabic"
arDZ ## "Algeria - Arabic"
arEG ## "Egypt - Arabic"
arIQ ## "Iraq - Arabic"
arJO ## "Jordan - Arabic"
arKW ## "Kuwait - Arabic"
arLB ## "Lebanon - Arabic"
arLY ## "Libya - Arabic"
arMA ## "Morocco - Arabic"
arOM ## "Oman - Arabic"
arQA ## "Qatar - Arabic"
arSA ## "Saudi Arabia - Arabic"
arSY ## "Syria - Arabic"
arTN ## "Tunisia - Arabic"
arYE ## "Yemen - Arabic"
arnCL ## "Chile - Mapudungun"
asIN ## "India - Assamese"
azAZ ## "Azerbaijan - Azeri (Cyrillic)"
baRU ## "Russia - Bashkir"
beBY ## "Belarus - Belarusian"
bgBG ## "Bulgaria - Bulgarian"
bnBD ## "Bangladesh - Bengali"
bnIN ## "India - Bengali"
boCN ## "People's Republic of China - Tibetan"
brFR ## "France - Breton"
bsBA ## "Bosnia and Herzegovina - Bosnian (Cyrillic)"
caES ## "Spain - Catalan"
coFR ## "France - Corsican"
csCZ ## "Czech Republic - Czech"
cyGB ## "United Kingdom - Welsh"
daDK ## "Denmark - Danish"
deAT ## "Austria - German"
deCH ## "Switzerland - German"
deDE ## "Germany - German"
deLI ## "Liechtenstein - German"
deLU ## "Luxembourg - German"
dsbDE ## "Germany - Lower Sorbian"
dvMV ## "Maldives - Divehi"
elGR ## "Greece - Greek"
en029 ## "Caribbean - English"
enAU ## "Australia - English"
enBZ ## "Belize - English"
enCA ## "Canada - English"
enGB ## "United Kingdom - English"
enIE ## "Ireland - English"
enIN ## "India - English"
enJM ## "Jamaica - English"
enMY ## "Malaysia - English"
enNZ ## "New Zealand - English"
enPH ## "Republic of the Philippines - English"
enSG ## "Singapore - English"
enTT ## "Trinidad and Tobago - English"
enUS ## "United States - English"
enZA ## "South Africa - English"
enZW ## "Zimbabwe - English"
esAR ## "Argentina - Spanish"
esBO ## "Bolivia - Spanish"
esCL ## "Chile - Spanish"
esCO ## "Colombia - Spanish"
esCR ## "Costa Rica - Spanish"
esDO ## "Dominican Republic - Spanish"
esEC ## "Ecuador - Spanish"
esES ## "Spain - Spanish"
esGT ## "Guatemala - Spanish"
esHN ## "Honduras - Spanish"
esMX ## "Mexico - Spanish"
esNI ## "Nicaragua - Spanish"
esPA ## "Panama - Spanish"
esPE ## "Peru - Spanish"
esPR ## "Puerto Rico - Spanish"
esPY ## "Paraguay - Spanish"
esSV ## "El Salvador - Spanish"
esUS ## "United States - Spanish"
esUY ## "Uruguay - Spanish"
esVE ## "Bolivarian Republic of Venezuela - Spanish"
etEE ## "Estonia - Estonian"
euES ## "Spain - Basque"
faIR ## "Iran - Persian"
fiFI ## "Finland - Finnish"
filPH ## "Philippines - Filipino"
foFO ## "Faroe Islands - Faroese"
frBE ## "Belgium - French"
frCA ## "Canada - French"
frCH ## "Switzerland - French"
frFR ## "France - French"
frLU ## "Luxembourg - French"
frMC ## "Principality of Monaco - French"
fyNL ## "Netherlands - Frisian"
gaIE ## "Ireland - Irish"
gdGB ## "United Kingdom - Scottish Gaelic"
glES ## "Spain - Galician"
gswFR ## "France - Alsatian"
guIN ## "India - Gujarati"
haNG ## "Nigeria - Hausa (Latin)"
heIL ## "Israel - Hebrew"
hiIN ## "India - Hindi"
hrBA ## "Bosnia and Herzegovina - Croatian"
hrHR ## "Croatia - Croatian"
hsbDE ## "Germany - Upper Sorbian"
huHU ## "Hungary - Hungarian"
hyAM ## "Armenia - Armenian"
idID ## "Indonesia - Indonesian"
igNG ## "Nigeria - Igbo"
iiCN ## "People's Republic of China - Yi"
isIS ## "Iceland - Icelandic"
itCH ## "Switzerland - Italian"
itIT ## "Italy - Italian"
iuCA ## "Canada - Inuktitut (Latin)"
jaJP ## "Japan - Japanese"
kaGE ## "Georgia - Georgian"
kkKZ ## "Kazakhstan - Kazakh"
klGL ## "Greenland - Greenlandic"
kmKH ## "Cambodia - Khmer"
knIN ## "India - Kannada"
koKR ## "Korea - Korean"
kokIN ## "India - Konkani"
kyKG ## "Kyrgyzstan - Kyrgyz"
lbLU ## "Luxembourg - Luxembourgish"
loLA ## "Lao P.D.R. - Lao"
ltLT ## "Lithuania - Lithuanian"
lvLV ## "Latvia - Latvian"
miNZ ## "New Zealand - Maori"
mkMK ## "Macedonia (FYROM) - Macedonian (FYROM)"
mlIN ## "India - Malayalam"
mnCN ## "People's Republic of China - Mongolian (Traditional Mongolian)"
mnMN ## "Mongolia - Mongolian (Cyrillic)"
mohCA ## "Canada - Mohawk"
mrIN ## "India - Marathi"
msBN ## "Brunei Darussalam - Malay"
msMY ## "Malaysia - Malay"
mtMT ## "Malta - Maltese"
nbNO ## "Norway - Norwegian (Bokmal)"
neNP ## "Nepal - Nepali"
nlBE ## "Belgium - Dutch"
nlNL ## "Netherlands - Dutch"
nnNO ## "Norway - Norwegian (Nynorsk)"
nsoZA ## "South Africa - Sesotho sa Leboa"
ocFR ## "France - Occitan"
orIN ## "India - Oriya"
paIN ## "India - Punjabi"
plPL ## "Poland - Polish"
prsAF ## "Afghanistan - Dari"
psAF ## "Afghanistan - Pashto"
ptBR ## "Brazil - Portuguese"
ptPT ## "Portugal - Portuguese"
qutGT ## "Guatemala - K'iche"
quzBO ## "Bolivia - Quechua"
quzEC ## "Ecuador - Quechua"
quzPE ## "Peru - Quechua"
rmCH ## "Switzerland - Romansh"
roRO ## "Romania - Romanian"
ruRU ## "Russia - Russian"
rwRW ## "Rwanda - Kinyarwanda"
saIN ## "India - Sanskrit"
sahRU ## "Russia - Yakut"
seFI ## "Finland - Sami (Northern)"
seNO ## "Norway - Sami (Northern)"
seSE ## "Sweden - Sami (Northern)"
siLK ## "Sri Lanka - Sinhala"
skSK ## "Slovakia - Slovak"
slSI ## "Slovenia - Slovenian"
smaNO ## "Norway - Sami (Southern)"
smaSE ## "Sweden - Sami (Southern)"
smjNO ## "Norway - Sami (Lule)"
smjSE ## "Sweden - Sami (Lule)"
smnFI ## "Finland - Sami (Inari)"
smsFI ## "Finland - Sami (Skolt)"
sqAL ## "Albania - Albanian"
srBA ## "Bosnia and Herzegovina - Serbian (Cyrillic)"
srCS ## "Serbia and Montenegro (Former) - Serbian (Cyrillic)"
srME ## "Montenegro - Serbian (Cyrillic)"
srRS ## "Serbia - Serbian (Cyrillic)"
svFI ## "Finland - Swedish"
svSE ## "Sweden - Swedish"
swKE ## "Kenya - Kiswahili"
syrSY ## "Syria - Syriac"
taIN ## "India - Tamil"
teIN ## "India - Telugu"
tgTJ ## "Tajikistan - Tajik (Cyrillic)"
thTH ## "Thailand - Thai"
tkTM ## "Turkmenistan - Turkmen"
tnZA ## "South Africa - Setswana"
trTR ## "Turkey - Turkish"
ttRU ## "Russia - Tatar"
tzmDZ ## "Algeria - Tamazight (Latin)"
ugCN ## "People's Republic of China - Uyghur"
ukUA ## "Ukraine - Ukrainian"
urPK ## "Islamic Republic of Pakistan - Urdu"
uzUZ ## "Uzbekistan - Uzbek (Cyrillic)"
viVN ## "Vietnam - Vietnamese"
woSN ## "Senegal - Wolof"
xhZA ## "South Africa - isiXhosa"
yoNG ## "Nigeria - Yoruba"
zhCN ## "People's Republic of China - Chinese (Simplified) Legacy"
zhHK ## "Hong Kong S.A.R. - Chinese (Traditional) Legacy"
zhMO ## "Macao S.A.R. - Chinese (Traditional) Legacy"
zhSG ## "Singapore - Chinese (Simplified) Legacy"
zhTW ## "Taiwan - Chinese (Traditional) Legacy"
zuZA ## "South Africa - isiZulu"
const
languageToName*: array[Language, cstring] = [
Language.afZA: cstring"South Africa - Afrikaans",
Language.amET: cstring"Ethiopia - Amharic",
Language.arAE: cstring"U.A.E. - Arabic",
Language.arBH: cstring"Bahrain - Arabic",
Language.arDZ: cstring"Algeria - Arabic",
Language.arEG: cstring"Egypt - Arabic",
Language.arIQ: cstring"Iraq - Arabic",
Language.arJO: cstring"Jordan - Arabic",
Language.arKW: cstring"Kuwait - Arabic",
Language.arLB: cstring"Lebanon - Arabic",
Language.arLY: cstring"Libya - Arabic",
Language.arMA: cstring"Morocco - Arabic",
Language.arOM: cstring"Oman - Arabic",
Language.arQA: cstring"Qatar - Arabic",
Language.arSA: cstring"Saudi Arabia - Arabic",
Language.arSY: cstring"Syria - Arabic",
Language.arTN: cstring"Tunisia - Arabic",
Language.arYE: cstring"Yemen - Arabic",
Language.arnCL: cstring"Chile - Mapudungun",
Language.asIN: cstring"India - Assamese",
Language.azAZ: cstring"Azerbaijan - Azeri (Cyrillic)",
Language.baRU: cstring"Russia - Bashkir",
Language.beBY: cstring"Belarus - Belarusian",
Language.bgBG: cstring"Bulgaria - Bulgarian",
Language.bnBD: cstring"Bangladesh - Bengali",
Language.bnIN: cstring"India - Bengali",
Language.boCN: cstring"People's Republic of China - Tibetan",
Language.brFR: cstring"France - Breton",
Language.bsBA: cstring"Bosnia and Herzegovina - Bosnian (Cyrillic)",
Language.caES: cstring"Spain - Catalan",
Language.coFR: cstring"France - Corsican",
Language.csCZ: cstring"Czech Republic - Czech",
Language.cyGB: cstring"United Kingdom - Welsh",
Language.daDK: cstring"Denmark - Danish",
Language.deAT: cstring"Austria - German",
Language.deCH: cstring"Switzerland - German",
Language.deDE: cstring"Germany - German",
Language.deLI: cstring"Liechtenstein - German",
Language.deLU: cstring"Luxembourg - German",
Language.dsbDE: cstring"Germany - Lower Sorbian",
Language.dvMV: cstring"Maldives - Divehi",
Language.elGR: cstring"Greece - Greek",
Language.en029: cstring"Caribbean - English",
Language.enAU: cstring"Australia - English",
Language.enBZ: cstring"Belize - English",
Language.enCA: cstring"Canada - English",
Language.enGB: cstring"United Kingdom - English",
Language.enIE: cstring"Ireland - English",
Language.enIN: cstring"India - English",
Language.enJM: cstring"Jamaica - English",
Language.enMY: cstring"Malaysia - English",
Language.enNZ: cstring"New Zealand - English",
Language.enPH: cstring"Republic of the Philippines - English",
Language.enSG: cstring"Singapore - English",
Language.enTT: cstring"Trinidad and Tobago - English",
Language.enUS: cstring"United States - English",
Language.enZA: cstring"South Africa - English",
Language.enZW: cstring"Zimbabwe - English",
Language.esAR: cstring"Argentina - Spanish",
Language.esBO: cstring"Bolivia - Spanish",
Language.esCL: cstring"Chile - Spanish",
Language.esCO: cstring"Colombia - Spanish",
Language.esCR: cstring"Costa Rica - Spanish",
Language.esDO: cstring"Dominican Republic - Spanish",
Language.esEC: cstring"Ecuador - Spanish",
Language.esES: cstring"Spain - Spanish",
Language.esGT: cstring"Guatemala - Spanish",
Language.esHN: cstring"Honduras - Spanish",
Language.esMX: cstring"Mexico - Spanish",
Language.esNI: cstring"Nicaragua - Spanish",
Language.esPA: cstring"Panama - Spanish",
Language.esPE: cstring"Peru - Spanish",
Language.esPR: cstring"Puerto Rico - Spanish",
Language.esPY: cstring"Paraguay - Spanish",
Language.esSV: cstring"El Salvador - Spanish",
Language.esUS: cstring"United States - Spanish",
Language.esUY: cstring"Uruguay - Spanish",
Language.esVE: cstring"Bolivarian Republic of Venezuela - Spanish",
Language.etEE: cstring"Estonia - Estonian",
Language.euES: cstring"Spain - Basque",
Language.faIR: cstring"Iran - Persian",
Language.fiFI: cstring"Finland - Finnish",
Language.filPH: cstring"Philippines - Filipino",
Language.foFO: cstring"Faroe Islands - Faroese",
Language.frBE: cstring"Belgium - French",
Language.frCA: cstring"Canada - French",
Language.frCH: cstring"Switzerland - French",
Language.frFR: cstring"France - French",
Language.frLU: cstring"Luxembourg - French",
Language.frMC: cstring"Principality of Monaco - French",
Language.fyNL: cstring"Netherlands - Frisian",
Language.gaIE: cstring"Ireland - Irish",
Language.gdGB: cstring"United Kingdom - Scottish Gaelic",
Language.glES: cstring"Spain - Galician",
Language.gswFR: cstring"France - Alsatian",
Language.guIN: cstring"India - Gujarati",
Language.haNG: cstring"Nigeria - Hausa (Latin)",
Language.heIL: cstring"Israel - Hebrew",
Language.hiIN: cstring"India - Hindi",
Language.hrBA: cstring"Bosnia and Herzegovina - Croatian",
Language.hrHR: cstring"Croatia - Croatian",
Language.hsbDE: cstring"Germany - Upper Sorbian",
Language.huHU: cstring"Hungary - Hungarian",
Language.hyAM: cstring"Armenia - Armenian",
Language.idID: cstring"Indonesia - Indonesian",
Language.igNG: cstring"Nigeria - Igbo",
Language.iiCN: cstring"People's Republic of China - Yi",
Language.isIS: cstring"Iceland - Icelandic",
Language.itCH: cstring"Switzerland - Italian",
Language.itIT: cstring"Italy - Italian",
Language.iuCA: cstring"Canada - Inuktitut (Latin)",
Language.jaJP: cstring"Japan - Japanese",
Language.kaGE: cstring"Georgia - Georgian",
Language.kkKZ: cstring"Kazakhstan - Kazakh",
Language.klGL: cstring"Greenland - Greenlandic",
Language.kmKH: cstring"Cambodia - Khmer",
Language.knIN: cstring"India - Kannada",
Language.koKR: cstring"Korea - Korean",
Language.kokIN: cstring"India - Konkani",
Language.kyKG: cstring"Kyrgyzstan - Kyrgyz",
Language.lbLU: cstring"Luxembourg - Luxembourgish",
Language.loLA: cstring"Lao P.D.R. - Lao",
Language.ltLT: cstring"Lithuania - Lithuanian",
Language.lvLV: cstring"Latvia - Latvian",
Language.miNZ: cstring"New Zealand - Maori",
Language.mkMK: cstring"Macedonia (FYROM) - Macedonian (FYROM)",
Language.mlIN: cstring"India - Malayalam",
Language.mnCN: cstring"People's Republic of China - Mongolian (Traditional Mongolian)",
Language.mnMN: cstring"Mongolia - Mongolian (Cyrillic)",
Language.mohCA: cstring"Canada - Mohawk",
Language.mrIN: cstring"India - Marathi",
Language.msBN: cstring"Brunei Darussalam - Malay",
Language.msMY: cstring"Malaysia - Malay",
Language.mtMT: cstring"Malta - Maltese",
Language.nbNO: cstring"Norway - Norwegian (Bokmal)",
Language.neNP: cstring"Nepal - Nepali",
Language.nlBE: cstring"Belgium - Dutch",
Language.nlNL: cstring"Netherlands - Dutch",
Language.nnNO: cstring"Norway - Norwegian (Nynorsk)",
Language.nsoZA: cstring"South Africa - Sesotho sa Leboa",
Language.ocFR: cstring"France - Occitan",
Language.orIN: cstring"India - Oriya",
Language.paIN: cstring"India - Punjabi",
Language.plPL: cstring"Poland - Polish",
Language.prsAF: cstring"Afghanistan - Dari",
Language.psAF: cstring"Afghanistan - Pashto",
Language.ptBR: cstring"Brazil - Portuguese",
Language.ptPT: cstring"Portugal - Portuguese",
Language.qutGT: cstring"Guatemala - K'iche",
Language.quzBO: cstring"Bolivia - Quechua",
Language.quzEC: cstring"Ecuador - Quechua",
Language.quzPE: cstring"Peru - Quechua",
Language.rmCH: cstring"Switzerland - Romansh",
Language.roRO: cstring"Romania - Romanian",
Language.ruRU: cstring"Russia - Russian",
Language.rwRW: cstring"Rwanda - Kinyarwanda",
Language.saIN: cstring"India - Sanskrit",
Language.sahRU: cstring"Russia - Yakut",
Language.seFI: cstring"Finland - Sami (Northern)",
Language.seNO: cstring"Norway - Sami (Northern)",
Language.seSE: cstring"Sweden - Sami (Northern)",
Language.siLK: cstring"Sri Lanka - Sinhala",
Language.skSK: cstring"Slovakia - Slovak",
Language.slSI: cstring"Slovenia - Slovenian",
Language.smaNO: cstring"Norway - Sami (Southern)",
Language.smaSE: cstring"Sweden - Sami (Southern)",
Language.smjNO: cstring"Norway - Sami (Lule)",
Language.smjSE: cstring"Sweden - Sami (Lule)",
Language.smnFI: cstring"Finland - Sami (Inari)",
Language.smsFI: cstring"Finland - Sami (Skolt)",
Language.sqAL: cstring"Albania - Albanian",
Language.srBA: cstring"Bosnia and Herzegovina - Serbian (Cyrillic)",
Language.srCS: cstring"Serbia and Montenegro (Former) - Serbian (Cyrillic)",
Language.srME: cstring"Montenegro - Serbian (Cyrillic)",
Language.srRS: cstring"Serbia - Serbian (Cyrillic)",
Language.svFI: cstring"Finland - Swedish",
Language.svSE: cstring"Sweden - Swedish",
Language.swKE: cstring"Kenya - Kiswahili",
Language.syrSY: cstring"Syria - Syriac",
Language.taIN: cstring"India - Tamil",
Language.teIN: cstring"India - Telugu",
Language.tgTJ: cstring"Tajikistan - Tajik (Cyrillic)",
Language.thTH: cstring"Thailand - Thai",
Language.tkTM: cstring"Turkmenistan - Turkmen",
Language.tnZA: cstring"South Africa - Setswana",
Language.trTR: cstring"Turkey - Turkish",
Language.ttRU: cstring"Russia - Tatar",
Language.tzmDZ: cstring"Algeria - Tamazight (Latin)",
Language.ugCN: cstring"People's Republic of China - Uyghur",
Language.ukUA: cstring"Ukraine - Ukrainian",
Language.urPK: cstring"Islamic Republic of Pakistan - Urdu",
Language.uzUZ: cstring"Uzbekistan - Uzbek (Cyrillic)",
Language.viVN: cstring"Vietnam - Vietnamese",
Language.woSN: cstring"Senegal - Wolof",
Language.xhZA: cstring"South Africa - isiXhosa",
Language.yoNG: cstring"Nigeria - Yoruba",
Language.zhCN: cstring"People's Republic of China - Chinese (Simplified) Legacy",
Language.zhHK: cstring"Hong Kong S.A.R. - Chinese (Traditional) Legacy",
Language.zhMO: cstring"Macao S.A.R. - Chinese (Traditional) Legacy",
Language.zhSG: cstring"Singapore - Chinese (Simplified) Legacy",
Language.zhTW: cstring"Taiwan - Chinese (Traditional) Legacy",
Language.zuZA: cstring"South Africa - isiZulu"]
languageToCode*: array[Language, cstring] = [
Language.afZA: cstring"af-ZA",
Language.amET: cstring"am-ET",
Language.arAE: cstring"ar-AE",
Language.arBH: cstring"ar-BH",
Language.arDZ: cstring"ar-DZ",
Language.arEG: cstring"ar-EG",
Language.arIQ: cstring"ar-IQ",
Language.arJO: cstring"ar-JO",
Language.arKW: cstring"ar-KW",
Language.arLB: cstring"ar-LB",
Language.arLY: cstring"ar-LY",
Language.arMA: cstring"ar-MA",
Language.arOM: cstring"ar-OM",
Language.arQA: cstring"ar-QA",
Language.arSA: cstring"ar-SA",
Language.arSY: cstring"ar-SY",
Language.arTN: cstring"ar-TN",
Language.arYE: cstring"ar-YE",
Language.arnCL: cstring"arn-CL",
Language.asIN: cstring"as-IN",
Language.azAZ: cstring"az-AZ",
Language.baRU: cstring"ba-RU",
Language.beBY: cstring"be-BY",
Language.bgBG: cstring"bg-BG",
Language.bnBD: cstring"bn-BD",
Language.bnIN: cstring"bn-IN",
Language.boCN: cstring"bo-CN",
Language.brFR: cstring"br-FR",
Language.bsBA: cstring"bs-BA",
Language.caES: cstring"ca-ES",
Language.coFR: cstring"co-FR",
Language.csCZ: cstring"cs-CZ",
Language.cyGB: cstring"cy-GB",
Language.daDK: cstring"da-DK",
Language.deAT: cstring"de-AT",
Language.deCH: cstring"de-CH",
Language.deDE: cstring"de-DE",
Language.deLI: cstring"de-LI",
Language.deLU: cstring"de-LU",
Language.dsbDE: cstring"dsb-DE",
Language.dvMV: cstring"dv-MV",
Language.elGR: cstring"el-GR",
Language.en029: cstring"en-029",
Language.enAU: cstring"en-AU",
Language.enBZ: cstring"en-BZ",
Language.enCA: cstring"en-CA",
Language.enGB: cstring"en-GB",
Language.enIE: cstring"en-IE",
Language.enIN: cstring"en-IN",
Language.enJM: cstring"en-JM",
Language.enMY: cstring"en-MY",
Language.enNZ: cstring"en-NZ",
Language.enPH: cstring"en-PH",
Language.enSG: cstring"en-SG",
Language.enTT: cstring"en-TT",
Language.enUS: cstring"en-US",
Language.enZA: cstring"en-ZA",
Language.enZW: cstring"en-ZW",
Language.esAR: cstring"es-AR",
Language.esBO: cstring"es-BO",
Language.esCL: cstring"es-CL",
Language.esCO: cstring"es-CO",
Language.esCR: cstring"es-CR",
Language.esDO: cstring"es-DO",
Language.esEC: cstring"es-EC",
Language.esES: cstring"es-ES",
Language.esGT: cstring"es-GT",
Language.esHN: cstring"es-HN",
Language.esMX: cstring"es-MX",
Language.esNI: cstring"es-NI",
Language.esPA: cstring"es-PA",
Language.esPE: cstring"es-PE",
Language.esPR: cstring"es-PR",
Language.esPY: cstring"es-PY",
Language.esSV: cstring"es-SV",
Language.esUS: cstring"es-US",
Language.esUY: cstring"es-UY",
Language.esVE: cstring"es-VE",
Language.etEE: cstring"et-EE",
Language.euES: cstring"eu-ES",
Language.faIR: cstring"fa-IR",
Language.fiFI: cstring"fi-FI",
Language.filPH: cstring"fil-PH",
Language.foFO: cstring"fo-FO",
Language.frBE: cstring"fr-BE",
Language.frCA: cstring"fr-CA",
Language.frCH: cstring"fr-CH",
Language.frFR: cstring"fr-FR",
Language.frLU: cstring"fr-LU",
Language.frMC: cstring"fr-MC",
Language.fyNL: cstring"fy-NL",
Language.gaIE: cstring"ga-IE",
Language.gdGB: cstring"gd-GB",
Language.glES: cstring"gl-ES",
Language.gswFR: cstring"gsw-FR",
Language.guIN: cstring"gu-IN",
Language.haNG: cstring"ha-NG",
Language.heIL: cstring"he-IL",
Language.hiIN: cstring"hi-IN",
Language.hrBA: cstring"hr-BA",
Language.hrHR: cstring"hr-HR",
Language.hsbDE: cstring"hsb-DE",
Language.huHU: cstring"hu-HU",
Language.hyAM: cstring"hy-AM",
Language.idID: cstring"id-ID",
Language.igNG: cstring"ig-NG",
Language.iiCN: cstring"ii-CN",
Language.isIS: cstring"is-IS",
Language.itCH: cstring"it-CH",
Language.itIT: cstring"it-IT",
Language.iuCA: cstring"iu-CA",
Language.jaJP: cstring"ja-JP",
Language.kaGE: cstring"ka-GE",
Language.kkKZ: cstring"kk-KZ",
Language.klGL: cstring"kl-GL",
Language.kmKH: cstring"km-KH",
Language.knIN: cstring"kn-IN",
Language.koKR: cstring"ko-KR",
Language.kokIN: cstring"kok-IN",
Language.kyKG: cstring"ky-KG",
Language.lbLU: cstring"lb-LU",
Language.loLA: cstring"lo-LA",
Language.ltLT: cstring"lt-LT",
Language.lvLV: cstring"lv-LV",
Language.miNZ: cstring"mi-NZ",
Language.mkMK: cstring"mk-MK",
Language.mlIN: cstring"ml-IN",
Language.mnCN: cstring"mn-CN",
Language.mnMN: cstring"mn-MN",
Language.mohCA: cstring"moh-CA",
Language.mrIN: cstring"mr-IN",
Language.msBN: cstring"ms-BN",
Language.msMY: cstring"ms-MY",
Language.mtMT: cstring"mt-MT",
Language.nbNO: cstring"nb-NO",
Language.neNP: cstring"ne-NP",
Language.nlBE: cstring"nl-BE",
Language.nlNL: cstring"nl-NL",
Language.nnNO: cstring"nn-NO",
Language.nsoZA: cstring"nso-ZA",
Language.ocFR: cstring"oc-FR",
Language.orIN: cstring"or-IN",
Language.paIN: cstring"pa-IN",
Language.plPL: cstring"pl-PL",
Language.prsAF: cstring"prs-AF",
Language.psAF: cstring"ps-AF",
Language.ptBR: cstring"pt-BR",
Language.ptPT: cstring"pt-PT",
Language.qutGT: cstring"qut-GT",
Language.quzBO: cstring"quz-BO",
Language.quzEC: cstring"quz-EC",
Language.quzPE: cstring"quz-PE",
Language.rmCH: cstring"rm-CH",
Language.roRO: cstring"ro-RO",
Language.ruRU: cstring"ru-RU",
Language.rwRW: cstring"rw-RW",
Language.saIN: cstring"sa-IN",
Language.sahRU: cstring"sah-RU",
Language.seFI: cstring"se-FI",
Language.seNO: cstring"se-NO",
Language.seSE: cstring"se-SE",
Language.siLK: cstring"si-LK",
Language.skSK: cstring"sk-SK",
Language.slSI: cstring"sl-SI",
Language.smaNO: cstring"sma-NO",
Language.smaSE: cstring"sma-SE",
Language.smjNO: cstring"smj-NO",
Language.smjSE: cstring"smj-SE",
Language.smnFI: cstring"smn-FI",
Language.smsFI: cstring"sms-FI",
Language.sqAL: cstring"sq-AL",
Language.srBA: cstring"sr-BA",
Language.srCS: cstring"sr-CS",
Language.srME: cstring"sr-ME",
Language.srRS: cstring"sr-RS",
Language.svFI: cstring"sv-FI",
Language.svSE: cstring"sv-SE",
Language.swKE: cstring"sw-KE",
Language.syrSY: cstring"syr-SY",
Language.taIN: cstring"ta-IN",
Language.teIN: cstring"te-IN",
Language.tgTJ: cstring"tg-TJ",
Language.thTH: cstring"th-TH",
Language.tkTM: cstring"tk-TM",
Language.tnZA: cstring"tn-ZA",
Language.trTR: cstring"tr-TR",
Language.ttRU: cstring"tt-RU",
Language.tzmDZ: cstring"tzm-DZ",
Language.ugCN: cstring"ug-CN",
Language.ukUA: cstring"uk-UA",
Language.urPK: cstring"ur-PK",
Language.uzUZ: cstring"uz-UZ",
Language.viVN: cstring"vi-VN",
Language.woSN: cstring"wo-SN",
Language.xhZA: cstring"xh-ZA",
Language.yoNG: cstring"yo-NG",
Language.zhCN: cstring"zh-CN",
Language.zhHK: cstring"zh-HK",
Language.zhMO: cstring"zh-MO",
Language.zhSG: cstring"zh-SG",
Language.zhTW: cstring"zh-TW",
Language.zuZA: cstring"zu-ZA"]

View file

@ -1,38 +0,0 @@
import macros, kbase
from strutils import `%`
macro fieldNamesAsArray*(t: typed; pattern = "$1"): untyped =
var impl = getTypeImpl(getTypeImpl(t)[1])
if impl.kind == nnkRefTy: impl = getTypeImpl(impl[0])
expectKind impl, nnkObjectTy
impl = impl[2]
expectKind impl, nnkRecList
result = newTree(nnkBracket)
for x in impl:
expectKind x, nnkIdentDefs
result.add(newCall(bindSym"kstring", newLit(pattern.strVal % $x[0])))
proc str(n: NimNode): NimNode =
if n.kind in {nnkStrLit, nnkTripleStrLit}:
result = newCall(bindSym"kstring", n)
else:
result = copyNimNode(n)
for i in 0..<n.len:
result.add str(n[i])
macro kstrLits*(x: untyped): untyped =
## Transforms every string literal "abc" to ``kstring"abc"``. This makes
## things much easier to write. String literals of the form ``r"abc"`` are
## not affected.
result = str(x)
when isMainModule:
type
MyObject = ref object
x*: string
next*: int
more, here*: float
const myObjectFields = fieldNamesAsArray(MyObject, "MyObject_$1")
for f in myObjectFields: echo f

View file

@ -1,380 +0,0 @@
import jdict
type
Message = enum
Unchanged
Changed
Mark
Inserted
Deleted
Replaced
State = object
stale: int
outdated: bool
phantom: bool
type
SinkSeq = seq[proc(msg: Message, pos: int)]
ReactiveBase* = ref object of RootObj ## everything that is a "reactive"
## value derives from that
sinks*: SinkSeq
dups: JDict[cstring, int]
id: int
Reactive*[T] = ref object of ReactiveBase
value*: T
RString* = Reactive[cstring]
# RTime* = Reactive[Time]
RInt* = Reactive[int]
RBool* = Reactive[bool]
RSeq*[T] = ref object of ReactiveBase
s: seq[T]
L: RInt
var rid: int
proc addSink(x: ReactiveBase; key: cstring; sink: proc(msg: Message; pos: int)) =
if x.dups == nil: x.dups = newJDict[cstring, int]()
if not x.dups.contains(key):
x.dups[key] = x.sinks.len
x.sinks.add sink
if x.id == 0:
inc rid
x.id = rid
else:
# update existing entry:
x.sinks[x.dups[key]] = sink
proc addSink(x: ReactiveBase; sink: proc(msg: Message; pos: int)) =
x.sinks.add sink
if x.id == 0:
inc rid
x.id = rid
var inhibited: int
proc broadcast(x: ReactiveBase, msg: Message; pos = 0) =
if inhibited == 0:
for s in x.sinks: s(msg, pos)
var toTrack: seq[(cstring, proc (msg: Message; pos: int))] = @[]
template withTrack(key, t, body) =
toTrack.add((key, t))
body
discard toTrack.pop()
proc trackDependency*(r: ReactiveBase) =
for t in toTrack:
r.addSink t[0], t[1]
template wrapObserver(f: untyped) =
var state: State
proc helper(msg: Message; pos: int) =
case msg.kind:
of Changed, Unchanged:
state.outdated = state.outdated or (msg.kind == Changed)
dec state.stale
if state.stale == 0:
if state.outdated:
var t = f()
let thisChanged = if x.value != t: Changed else: Unchanged
x.value = t
x.broadcast(thisChanged, pos)
state.outdated = false
else:
x.broadcast(Unchanged, pos)
of Mark:
if state.stale == 0: x.broadcast(msg, pos)
inc state.stale
of Inserted, Deleted, Replaced:
dec state.stale
if state.stale == 0:
x.broadcast(msg, pos)
#state.phantom = true
helper
template glitchFree(f: untyped) =
var state: State
proc helper(msg: Message; pos: int) =
case msg.kind:
of Changed, Unchanged:
state.outdated = state.outdated or (msg.kind == Changed)
dec state.stale
if state.stale == 0:
if state.outdated:
x.value = f
x.broadcast(msg.kind, pos)
state.outdated = false
else:
x.broadcast(Unchanged, pos)
of Mark:
if state.stale == 0: x.broadcast(msg, pos)
inc state.stale
of Inserted, Deleted, Replaced:
dec state.stale
if state.stale == 0:
x.broadcast(msg, pos)
helper
proc `<-`*[T](x: Reactive[T], val: T) =
#if x.value != val:
x.value = val
x.broadcast(Mark)
x.broadcast(Changed)
proc notifyObservers*(x: ReactiveBase) =
x.broadcast(Mark)
x.broadcast(Changed)
proc subscribe*[T](x: Reactive[T], f: proc(x: T)) =
let reactor = proc (msg: Message, pos: int) =
case msg:
of Mark: discard
of Changed, Unchanged: f(x.value)
of Inserted, Deleted, Replaced: discard
x.addSink reactor
proc subscribeSelf*[T: ReactiveBase](x: T, f: proc()) =
let reactor = proc (msg: Message, pos: int) =
case msg:
of Mark: discard
of Changed, Unchanged: f()
of Inserted, Deleted, Replaced: discard
x.addSink reactor
template lift1(op: untyped) =
proc op[T](a: Reactive[T]): (proc(): T) =
result = proc(): T = op(a.now)
template lift2(op: untyped) =
proc op[T](a, b: Reactive[T]): (proc(): T) =
result = proc(): T = op(a.now, b.now)
lift1 `not`
lift2 `&`
proc newReactive*[T](x: T): Reactive[T] =
result = Reactive[T](value: x)
proc rstr*(x: cstring): RString =
result = RString(value: x)
proc newRSeq*[T](len: int = 0): RSeq[T] =
result = RSeq[T](s: newSeq[T](len), L: newReactive[int](0))
proc newRSeq*[T](data: seq[T]): RSeq[T] =
result = RSeq[T](s: newSeq[T](data.len), L: newReactive[int](0))
for i in 0..high(data):
result.s[i] = data[i]
proc `[]=`*[T](x: RSeq[T]; index: int; v: T) =
x.s[index] = v
x.broadcast(Mark)
x.broadcast(Replaced, index)
proc `[]`*[T](x: RSeq[T]; index: int): T = x.s[index]
proc len*[T](x: RSeq[T]): int = x.s.len
proc add*[T](x: RSeq[T]; y: T) =
let position = x.s.len
x.s.add(y)
x.broadcast(Mark)
x.broadcast(Inserted, position)
proc insert*[T](x: RSeq[T]; y: T; position = 0) =
x.s.insert(y, position)
x.broadcast(Mark)
x.broadcast(Inserted, position)
proc delete*[T](x: RSeq[T]; position = 0) =
x.s.delete(position)
x.broadcast(Mark)
x.broadcast(Deleted, position)
proc deleteElem*[T](x: RSeq[T]; y: T) =
var position = -1
for i in 0..<x.len:
if x[i] == y:
position = i; break
if position >= 0:
x.s.delete(position)
x.broadcast(Mark)
x.broadcast(Deleted, position)
when false:
proc `:=`[T](x: Reactive[T], f: proc(): T) =
toTrack = wrapObserver(f())
x.value = f()
toTrack = nil
proc map*[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] =
let xl = x.L.value
let res = newRSeq[U](xl)
for i in 0..<xl:
res.s[i] := proc(): U = f(x[i].now)
let reactor = proc (msg: Message, pos: int) =
case msg:
of Mark, Changed, Unchanged: discard "nothing to do"
of Inserted:
res.insert(f(x[pos]), pos)
of Deleted:
res.delete(pos)
x.addSink reactor
result = res
import macros
template protect(body: untyped) =
#var tmp: seq[proc(msg: Message, pos: int)]
#swap(r.sinks, tmp)
inc inhibited
body
dec inhibited
#swap(r.sinks, tmp)
template trackStart(key: cstring; a, b: untyped) =
proc differ(m: Message; pos: int) =
if m == Changed:
protect:
karax.runDiff(kxi, a, b)
toTrack.add((key, differ))
template trackEnd() =
discard toTrack.pop()
template doTrack(r: ReactiveBase; a, b: untyped) {.dirty.} =
bind addSink, Message, RSeq, Changed, Deleted, Inserted
addSink r, proc(m: Message; pos: int) =
#when r is RSeq:
#echo "Message: ", m, " ", pos
if m == Changed:
protect r:
karax.runDiff(kxi, a, b)
template doTrackResize(r: ReactiveBase; a, b: untyped) {.dirty.} =
bind addSink, Message, RSeq, Changed, Deleted, Inserted
addSink r, proc(m: Message; pos: int) =
#when r is RSeq:
#echo "Message: ", m, " ", pos
case m
of Deleted: karax.runDel(kxi, a, pos)
of Inserted:
let it {.used.} = r[pos]
karax.runIns(kxi, a, b, pos)
else: discard
macro track*(procDef: untyped): untyped =
let params = params(procDef)
var trackings = newStmtList()
var inner = copyNimTree(procDef)
inner[0] = ident($procDef.name & "Inner")
var call = newCall(inner[0])
for j in 1..<params.len:
let x = params[j]
expectKind x, nnkIdentDefs
for i in 0..x.len-3:
let param = x[i]
call.add(param)
trackings.add inner
let key = newCall("cstring", newLit lineInfo(procDef))
trackings.add getAst(trackStart(key, ident"result", call))
trackings.add newAssignment(ident"result", call)
trackings.add getAst(trackEnd())
result = copyNimTree(procDef)
result.body = trackings
when defined(debugKaraxDsl):
echo repr result
proc generatePrivateAccessors(name, hidden, typ, fieldTyp: NimNode): NimNode =
template helper(name, hidden, typ, fieldTyp) {.dirty.} =
proc name(self: typ): fieldTyp =
trackDependency(self)
result = self.hidden
proc `name=`(self: typ; val: fieldTyp) =
self.hidden = val
notifyObservers(self)
result = getAst(helper(name, hidden, typ, fieldTyp))
proc generatePublicAccessors(name, hidden, typ, fieldTyp: NimNode): NimNode =
template helper(name, hidden, typ, fieldTyp) {.dirty.} =
proc name*(self: typ): fieldTyp =
trackDependency(self)
result = self.hidden
proc `name=`*(self: typ; val: fieldTyp) =
self.hidden = val
notifyObservers(self)
result = getAst(helper(name, hidden, typ, fieldTyp))
proc transform(n: NimNode; stmts, obj: NimNode): NimNode =
if n.kind == nnkIdentDefs and obj.kind == nnkIdent:
for i in 0..n.len-3:
let it = n[i]
let itB = if it.kind == nnkPostFix: it[1] else: it
let hidden = newIdentNode("raw" & $itB)
if it.kind == nnkPostFix:
stmts.add generatePublicAccessors(itB, hidden, obj, n[n.len-2])
else:
stmts.add generatePrivateAccessors(itB, hidden, obj, n[n.len-2])
n[i] = hidden
result = n
else:
var objB = if n.kind == nnkTypeDef: n[0] else: obj
result = copyNimNode(n)
for i in 0..<n.len:
if n.kind == nnkObjectTy and i == 1 and n[1].kind == nnkEmpty:
result.add newTree(nnkOfInherit, ident("ReactiveBase"))
else:
result.add transform(n[i], stmts, objB)
macro makeReactive*(n: untyped): untyped =
var a = newStmtList()
a.add newEmptyNode()
let t = transform(n, a, newEmptyNode())
a[0] = t
result = a
when defined(debugKaraxDsl):
echo repr result
import vdom
template vmap*(x: RSeq; elem, f: untyped): VNode =
let tmp = buildHtml(elem):
for i in 0..<len(x):
f(x[i])
doTrackResize(x, tmp, f(x[pos]))
tmp
template vmapIt*(x: RSeq; elem, call: untyped): VNode =
var it {.inject.}: type(x[0])
let tmp = buildHtml(elem):
for i in 0..<len(x):
it = x[i]
call
doTrackResize(x, tmp, call)
tmp
proc text*(s: RString): VNode =
result = text(s.value)
s.subscribe proc(v: cstring) =
if result.dom != nil: result.dom.nodeValue = v
when isMainModule:
makeReactive:
type
Foo = ref object of ReactiveBase
x, y: int
Bar = ref object of ReactiveBase
z: string
exported*: int
Another = ref object
a, b: cstring
c: bool

View file

@ -1,129 +0,0 @@
import os, strutils, browsers,times, tables
import parseopt
import threadpool
import static_server
const
css = """
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
"""
const html = """
<!DOCTYPE html>
<html>
<head>
<meta content="width=device-width, initial-scale=1" name="viewport" />
<title>$1</title>
$2
</head>
<body id="body" class="site">
<div id="ROOT"></div>
<script type="text/javascript" src="/app.js"></script>
$3
</body>
</html>
"""
const websocket = """
<script type="text/javascript">
var ws = new WebSocket("ws://localhost:8080/ws");
ws.onopen = function(evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
ws.onmessage = function(evt) {
console.log( "Received Message: " + evt.data);
if(evt.data == "refresh"){
window.location.href = window.location.href
}
};
ws.onclose = function(evt) {
console.log("Connection closed.");
};
</script>
"""
proc exec(cmd: string) =
if os.execShellCmd(cmd) != 0:
quit "External command failed: " & cmd
proc build(rest: string, selectedCss: string, run: bool, watch: bool) =
echo("Building...")
let cmd = "nim js --out:" & "app" & ".js " & rest
if watch:
discard os.execShellCmd(cmd)
else:
exec cmd
let dest = "app" & ".html"
let script = if watch: websocket else: ""
writeFile(dest, html % ["app", selectedCss, script])
if run: openDefaultBrowser("http://localhost:8080")
proc watchBuild(filePath: string, selectedCss: string, rest: string) {.thread.} =
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
while true:
sleep(300)
for path in walkDirRec("."):
if ".git" in path:
continue
var (_, _, ext) = splitFile(path)
if ext in [".scss",".sass",".less",".styl",".pcss",".postcss"]:
continue
if files.hasKey(path):
if files[path] != getLastModificationTime(path):
echo("File changed: " & path)
build(rest,selectedCss, false, true)
files[path] = getLastModificationTime(path)
else:
if absolutePath(path) in [absolutePath("app" & ".js"),absolutePath("app" & ".html")]:
continue
files[path] = getLastModificationTime(path)
proc serve(){.thread.} =
serveStatic()
proc main =
var op = initOptParser()
var rest = op.cmdLineRest
var file = ""
var run = false
var watch = false
var selectedCss = ""
while true:
op.next()
case op.kind
of cmdLongOption:
case op.key
of "run":
run = true
rest = rest.replace("--run ")
of "css":
if op.val != "":
selectedCss = readFile(op.val)
else:
selectedCss = css
rest = rest.substr(rest.find(" "))
else: discard
of cmdShortOption:
if op.key == "r":
run = true
rest = rest.replace("-r ")
if op.key == "w":
watch = true
rest = rest.replace("-w ")
of cmdArgument: file = op.key
of cmdEnd: break
if file.len == 0: quit "filename expected"
if run:
spawn serve()
if watch:
spawn watchBuild(file, selectedCss, rest)
build(rest,selectedCss, run, watch)
sync()
main()

View file

@ -1 +0,0 @@
switch("threads", "on")

View file

@ -1,141 +0,0 @@
import
std/[net, os, strutils, uri, mimetypes, asyncnet, asyncdispatch, md5,
logging, httpcore, asyncfile, asynchttpserver, tables, times]
from cgi import decodeUrl
import ws, dotenv
var logger = newConsoleLogger()
addHandler(logger)
when defined(release):
setLogFilter(lvlError)
type
RawHeaders* = seq[tuple[key, val: string]]
proc toStr(headers: RawHeaders): string =
$newHttpHeaders(headers)
proc send(request: Request, code: HttpCode, headers: RawHeaders,
body: string): Future[void] =
return request.respond(code, body, newHttpHeaders(headers))
proc statusContent(request: Request, status: HttpCode, content: string,
headers: RawHeaders): Future[void] =
try:
result = send(request, status, headers, content)
debug(" ", status, " ", toStr(headers))
except:
error("Could not send response: ", osErrorMsg(osLastError()))
proc sendStaticIfExists(req: Request, paths: seq[string]): Future[HttpCode] {.async.} =
result = Http200
let mimes = newMimetypes()
for p in paths:
if fileExists(p):
if fpOthersRead notin getFilePermissions(p):
return Http403
let fileSize = getFileSize(p)
let extPos = searchExtPos(p)
let mimetype = mimes.getMimetype(
if extPos >= 0: p.substr(extPos + 1)
else: "")
if fileSize < 10_000_000: # 10 mb
var file = readFile(p)
var hashed = getMD5(file)
# If the user has a cached version of this file and it matches our
# version, let them use it
if req.headers.getOrDefault("If-None-Match") == hashed:
await req.statusContent(Http304, "", default(RawHeaders))
else:
await req.statusContent(Http200, file, @{
"Content-Type": mimetype,
"ETag": hashed
})
else:
let headers = @{
"Content-Type": mimetype,
"Content-Length": $fileSize
}
await req.statusContent(Http200, "", headers)
var fileStream = newFutureStream[string]("sendStaticIfExists")
var file = openAsync(p, fmRead)
# Let `readToStream` write file data into fileStream in the
# background.
asyncCheck file.readToStream(fileStream)
# The `writeFromStream` proc will complete once all the data in the
# `bodyStream` has been written to the file.
while true:
let (hasValue, value) = await fileStream.read()
if hasValue:
await req.client.send(value)
else:
break
file.close()
return
# If we get to here then no match could be found.
return Http404
proc handleFileRequest(req: Request): Future[HttpCode] {.async.} =
# Find static file.
var reqPath = cgi.decodeUrl(req.url.path)
var staticDir = getEnv("staticDir") # it's assumed a relative dir
var status = Http400
var path = staticDir / reqPath
normalizePathEnd(path, false)
if dirExists(path):
status = await sendStaticIfExists(req, @[path / "index.html", path / "index.htm"])
else:
status = await sendStaticIfExists(req, @[path])
return status
proc handleWs(req: Request) {.async.} =
var ws = await newWebSocket(req)
await ws.send("Welcome to simple echo server")
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
let watchedFiles = [absolutePath "app.js", absolutePath "app.html"]
for path in watchedFiles:
files[path] = getLastModificationTime(path)
while ws.readyState == Open:
await sleepAsync(500)
var changed = false
for path in watchedFiles:
if files[path] != getLastModificationTime(path):
changed = true
files[path] = getLastModificationTime(path)
if changed:
await ws.send("refresh")
changed = false
proc serveStatic*() =
if fileExists("static.env"):
var env: DotEnv
env = initDotEnv(getCurrentDir(), "static.env")
env.overload()
else:
putEnv("staticDir", "assets/")
var server = newAsyncHttpServer()
proc cb(req: Request) {.gcsafe, async.} =
if req.url.path == "/ws":
await handleWs(req)
if req.url.path == "/":
await req.respond(Http200, readFile "app.html")
elif req.url.path == "/app.js":
let file = absolutePath("app" & ".js")
if not file.fileExists:
error(file, " does not exist!")
if fpUserRead notin os.getFilePermissions(file):
error("Could not read ", file, "!")
await req.respond(Http200, readFile(file))
else:
let status = await handleFileRequest(req)
if status != Http200:
await req.respond(status, "")
waitFor server.serve(Port(8080), cb)
when isMainModule:
serveStatic()

View file

@ -1,490 +0,0 @@
## Virtual DOM implementation for Karax.
when defined(js):
from kdom import Event, Node
else:
type
Event* = ref object
Node* = ref object
import macros, vstyles, kbase
from strutils import toUpperAscii, toLowerAscii, tokenize
type
VNodeKind* {.pure.} = enum
text = "#text", int = "#int", bool = "#bool",
vthunk = "#vthunk", dthunk = "#dthunk",
component = "#component", verbatim = "#verbatim",
html, head, title, base, link, meta, style,
script, noscript,
body, section, nav, article, aside,
h1, h2, h3, h4, h5, h6,
header, footer, address, main,
p, hr, pre, blockquote, ol, ul, li,
dl, dt, dd,
figure, figcaption,
tdiv = "div",
a, em, strong, small,
strikethrough = "s", cite, quote,
dfn, abbr, data, time, code, `var` = "var", samp,
kdb, sub, sup, italic = "i", bold = "b", underlined = "u",
mark, ruby, rt, rp, bdi, dbo, span, br, wbr,
ins, del, img, iframe, embed, `object` = "object",
param, video, audio, source, track, canvas, map,
area, math,
# SVG elements, see: https://www.w3.org/TR/SVG2/eltindex.html
animate, animateMotion, animateTransform, circle, clipPath, defs, desc,
`discard` = "discard", ellipse, feBlend, feColorMatrix, feComponentTransfer,
feComposite, feConvolveMatrix, feDiffuseLighting, feDisplacementMap,
feDistantLight, feDropShadow, feFlood, feFuncA, feFuncB, feFuncG, feFuncR,
feGaussianBlur, feImage, feMerge, feMergeNode, feMorphology, feOffset,
fePointLight, feSpecularLighting, feSpotLight, feTile, feTurbulence,
filter, foreignObject, g, image, line, linearGradient, marker, mask,
metadata, mpath, path, pattern, polygon, polyline, radialGradient, rect,
`set` = "set", stop, svg, switch, symbol, txt = "text", textPath, tspan,
unknown, use, view,
table, caption, colgroup, col, tbody, thead,
tfoot, tr, td, th,
form, fieldset, legend, label, input, button,
select, datalist, optgroup, option, textarea,
keygen, output, progress, meter,
details, summary, command, menu
const selfClosing = {area, base, br, col, embed, hr, img, input,
link, meta, param, source, track, wbr}
type
EventKind* {.pure.} = enum ## The events supported by the virtual DOM.
onclick, ## An element is clicked.
oncontextmenu, ## An element is right-clicked.
ondblclick, ## An element is double clicked.
onkeyup, ## A key was released.
onkeydown, ## A key is pressed.
onkeypressed, # A key was pressed.
onfocus, ## An element got the focus.
onblur, ## An element lost the focus.
onchange, ## The selected value of an element was changed.
onscroll, ## The user scrolled within an element.
onmousedown, ## A pointing device button (usually a mouse) is pressed
## on an element.
onmouseenter, ## A pointing device is moved onto the element that
## has the listener attached.
onmouseleave, ## A pointing device is moved off the element that
## has the listener attached.
onmousemove, ## A pointing device is moved over an element.
onmouseout, ## A pointing device is moved off the element that
## has the listener attached or off one of its children.
onmouseover, ## A pointing device is moved onto the element that has
## the listener attached or onto one of its children.
onmouseup, ## A pointing device button is released over an element.
ondrag, ## An element or text selection is being dragged (every 350ms).
ondragend, ## A drag operation is being ended (by releasing a mouse button
## or hitting the escape key).
ondragenter, ## A dragged element or text selection enters a valid drop target.
ondragleave, ## A dragged element or text selection leaves a valid drop target.
ondragover, ## An element or text selection is being dragged over a valid
## drop target (every 350ms).
ondragstart, ## The user starts dragging an element or text selection.
ondrop, ## An element is dropped on a valid drop target.
onsubmit, ## A form is submitted
oninput, ## An input value changes
onanimationstart,
onanimationend,
onanimationiteration,
onkeyupenter, ## vdom extension: an input field received the ENTER key press
onkeyuplater, ## vdom extension: a key was pressed and some time
## passed (useful for on-the-fly text completions)
onload, # img
ontransitioncancel,
ontransitionend,
ontransitionrun,
ontransitionstart,
onwheel # fires when the user rotates a wheel button on a pointing device.
macro buildLookupTables(): untyped =
var a = newTree(nnkBracket)
for i in low(VNodeKind)..high(VNodeKind):
let x = $i
let y = if x[0] == '#': x else: toUpperAscii(x)
a.add(newCall("kstring", newLit(y)))
var e = newTree(nnkBracket)
for i in low(EventKind)..high(EventKind):
e.add(newCall("kstring", newLit(substr($i, 2))))
template tmpl(a, e) {.dirty.} =
const
toTag*: array[VNodeKind, kstring] = a
toEventName*: array[EventKind, kstring] = e
result = getAst tmpl(a, e)
buildLookupTables()
type
EventHandler* = proc (ev: Event; target: VNode) {.closure.}
NativeEventHandler* = proc (ev: Event) {.closure.}
EventHandlers* = seq[(EventKind, EventHandler, NativeEventHandler)]
VKey* = kstring
VNode* = ref object of RootObj
kind*: VNodeKind
index*: int ## a generally useful 'index'
id*, class*, text*: kstring
kids: seq[VNode]
# even index: key, odd index: value; done this way for memory efficiency:
attrs: seq[kstring]
events*: EventHandlers
when false:
hash*: Hash
validHash*: bool
style*: VStyle ## the style that should be applied to the virtual node.
dom*: Node ## the attached real DOM node. Can be 'nil' if the virtual node
## is not part of the virtual DOM anymore.
VComponent* = ref object of VNode ## The abstract class for every karax component.
key*: VKey ## key that determines if two components are
## identical.
renderImpl*: proc(self: VComponent): VNode
changedImpl*: proc(self, newInstance: VComponent): bool
updatedImpl*: proc(self, newInstance: VComponent)
onAttachImpl*: proc(self: VComponent)
onDetachImpl*: proc(self: VComponent)
version*: int ## Update this to trigger a redraw by karax. Usually you
## should call 'markDirty' instead which is an alias for
## 'inc version'.
renderedVersion*: int ## Do not touch. Used by karax. The last version of the
## component we rendered.
expanded*: VNode ## Do not touch. Used by karax. The VDOM the component
## expanded to.
debugId*: int
proc value*(n: VNode): kstring = n.text
proc `value=`*(n: VNode; v: kstring) = n.text = v
proc intValue*(n: VNode): int = n.index
proc vn*(i: int): VNode = VNode(kind: VNodeKind.int, index: i)
proc vn*(b: bool): VNode = VNode(kind: VNodeKind.int, index: ord(b))
proc vn*(x: kstring): VNode = VNode(kind: VNodeKind.text, index: -1, text: x)
template callThunk*(fn: typed; n: VNode): untyped =
## for internal usage only.
fn(n.kids)
proc vthunk*(name: kstring; args: varargs[VNode, vn]): VNode =
VNode(kind: VNodeKind.vthunk, text: name, index: -1, kids: @args)
proc dthunk*(dom: Node): VNode =
VNode(kind: VNodeKind.dthunk, dom: dom)
proc setEventIfNoConflict(v: VNode; kind: EventKind; handler: EventHandler) =
assert handler != nil
for i in 0..<v.events.len:
if v.events[i][0] == kind:
#v.events[i][1] = handler
return
v.events.add((kind, handler, nil))
proc mergeEvents*(v: VNode; handlers: EventHandlers) =
## Overrides or adds the event handlers to `v`'s internal event handler list.
for h in handlers: v.setEventIfNoConflict(h[0], h[1])
proc defaultChangedImpl*(v, newInstance: VComponent): bool =
## The default implementation of 'changed'.
result = v.key != newInstance.key or v.version != v.renderedVersion
proc defaultUpdatedImpl*(v, newInstance: VComponent) =
discard
var gid = 0
proc getDebugId(): int =
inc(gid)
gid
template newComponent*[T](t: typeDesc[T];
render: (proc(self: VComponent): VNode) = nil,
onAttach: proc(self: VComponent) = nil,
onDetach: proc(self: VComponent) = nil,
changed: (proc(self, newInstance: VComponent): bool) = defaultChangedImpl,
updated: proc(self, newInstance: VComponent) = defaultUpdatedImpl): T =
## Use this template to create new components.
T(kind: VNodeKind.component, index: -1,
text: kstring(astToStr(t)), renderImpl: render,
changedImpl: changed, updatedImpl: updated,
onAttachImpl: onAttach, onDetachImpl: onDetach,
debugId: getDebugId())
template markDirty*(c: VComponent) =
## mark the component as dirty so that it is re-rendered.
inc c.version
proc setAttr*(n: VNode; key: kstring; val: kstring = "") =
if n.attrs.len == 0:
n.attrs = @[key, val]
else:
for i in countup(0, n.attrs.len-2, 2):
if n.attrs[i] == key:
n.attrs[i+1] = val
return
n.attrs.add key
n.attrs.add val
proc getAttr*(n: VNode; key: kstring): kstring =
for i in countup(0, n.attrs.len-2, 2):
if n.attrs[i] == key: return n.attrs[i+1]
proc takeOverAttr*(newNode, oldNode: VNode) =
shallowCopy oldNode.attrs, newNode.attrs
proc takeOverFields*(newNode, oldNode: VNode) =
template take(field) =
shallowCopy oldNode.field, newNode.field
take kind
take index
take id
take class
take text
take kids
take attrs
take events
take style
take dom
proc len*(x: VNode): int = x.kids.len
proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx]
proc `[]=`*(x: VNode; idx: int; y: VNode) = x.kids[idx] = y
proc add*(parent, kid: VNode) =
when not defined(js) and not defined(nimNoNil):
if parent.kids.isNil: parent.kids = @[]
parent.kids.add kid
proc delete*(parent: VNode; position: int) =
parent.kids.delete(position)
proc insert*(parent, kid: VNode; position: int) =
parent.kids.insert(kid, position)
proc newVNode*(kind: VNodeKind): VNode = VNode(kind: kind, index: -1)
proc tree*(kind: VNodeKind; kids: varargs[VNode]): VNode =
result = newVNode(kind)
for k in kids: result.add k
proc tree*(kind: VNodeKind; attrs: openarray[(kstring, kstring)];
kids: varargs[VNode]): VNode =
result = tree(kind, kids)
for a in attrs: result.setAttr(a[0], a[1])
when defined(js):
proc text*(s: string): VNode = VNode(kind: VNodeKind.text, text: kstring(s), index: -1)
proc text*(s: kstring): VNode = VNode(kind: VNodeKind.text, text: s, index: -1)
when defined(js):
proc verbatim*(s: string): VNode =
VNode(kind: VNodeKind.verbatim, text: kstring(s), index: -1)
proc verbatim*(s: kstring): VNode =
VNode(kind: VNodeKind.verbatim, text: s, index: -1)
iterator items*(n: VNode): VNode =
for i in 0..<n.kids.len: yield n.kids[i]
iterator attrs*(n: VNode): (kstring, kstring) =
for i in countup(0, n.attrs.len-2, 2):
yield (n.attrs[i], n.attrs[i+1])
proc sameAttrs*(a, b: VNode): bool =
if a.attrs.len == b.attrs.len:
result = true
for i in 0 ..< a.attrs.len:
if a.attrs[i] != b.attrs[i]: return false
proc addEventListener*(n: VNode; event: EventKind; handler: EventHandler) =
n.events.add((event, handler, nil))
when kstring is cstring:
proc len(a: kstring): int =
# xxx: maybe move where kstring is defined
# without this, `n.field.len` fails on js (non web) platform
if a == nil: 0 else: a.len
template toStringAttr(field) =
if n.field.len > 0:
result.add " " & astToStr(field) & " = " & $n.field
proc toString*(n: VNode; result: var string; indent: int) =
for i in 1..indent: result.add ' '
if result.len > 0: result.add '\L'
result.add "<" & $n.kind
toStringAttr(id)
toStringAttr(class)
for k, v in attrs(n):
result.add " " & $k & " = " & $v
result.add ">\L"
if n.kind == VNodeKind.text:
result.add n.text
else:
if n.text.len > 0:
result.add " value = "
result.add n.text
for child in items(n):
toString(child, result, indent+2)
for i in 1..indent: result.add ' '
result.add "\L</" & $n.kind & ">"
when false:
proc calcHash*(n: VNode) =
if n.validHash: return
n.validHash = true
var h: Hash = ord n.kind
if n.id != nil:
h &= "id"
h &= n.id
if n.class != nil:
h &= "class"
h &= n.class
if n.key >= 0:
h &= "k"
h &= n.key
for k, v in attrs(n):
h &= " "
h &= k
h &= "="
h &= v
if n.kind == VNodeKind.text or n.text != nil:
h &= "t"
h &= n.text
else:
for child in items(n):
calcHash(child)
h &= child.hash
n.hash = h
proc add*(result: var string, n: VNode, indent = 0, indWidth = 2) =
## adds the textual representation of `n` to `result`.
proc addEscapedAttr(result: var string, s: kstring) =
# `addEscaped` alternative with less escaped characters.
# Only to be used for escaping attribute values enclosed in double quotes!
for c in items(s):
case c
of '<': result.add("&lt;")
of '>': result.add("&gt;")
of '&': result.add("&amp;")
of '"': result.add("&quot;")
else: result.add(c)
proc addEscaped(result: var string, s: kstring) =
## same as ``result.add(escape(s))``, but more efficient.
for c in items(s):
case c
of '<': result.add("&lt;")
of '>': result.add("&gt;")
of '&': result.add("&amp;")
of '"': result.add("&quot;")
of '\'': result.add("&#x27;")
of '/': result.add("&#x2F;")
else: result.add(c)
proc addIndent(result: var string, indent: int) =
result.add("\n")
for i in 1..indent: result.add(' ')
if n.kind == VNodeKind.text:
result.addEscaped(n.text)
elif n.kind == VNodeKind.verbatim:
result.add(n.text)
else:
let kind = $n.kind
result.add('<')
result.add(kind)
if n.id.len > 0:
result.add " id=\""
result.addEscapedAttr(n.id)
result.add('"')
if n.class.len > 0:
result.add " class=\""
result.addEscapedAttr(n.class)
result.add('"')
for k, v in attrs(n):
result.add(' ')
result.add(k)
result.add("=\"")
result.addEscapedAttr(v)
result.add('"')
if n.style != nil:
result.add " style=\""
for k, v in pairs(n.style):
if v.len == 0: continue
for t in tokenize($k, seps={'A' .. 'Z'}):
if t.isSep: result.add '-'
result.add toLowerAscii(t.token)
result.add ": "
result.add v
result.add "; "
result.add('"')
if n.len > 0:
result.add('>')
if n.len > 1:
var noWhitespace = false
for i in 0..<n.len:
if n[i].kind == VNodeKind.text:
noWhitespace = true
break
if noWhitespace:
# for mixed leaves, we cannot output whitespace for readability,
# because this would be wrong. For example: ``a<b>b</b>`` is
# different from ``a <b>b</b>``.
for i in 0..<n.len: result.add(n[i], indent+indWidth, indWidth)
else:
for i in 0..<n.len:
result.addIndent(indent+indWidth)
result.add(n[i], indent+indWidth, indWidth)
result.addIndent(indent)
else:
result.add(n[0], indent+indWidth, indWidth)
result.add("</")
result.add(kind)
result.add(">")
elif n.kind in selfClosing:
result.add(" />")
else:
result.add(">")
result.add("</")
result.add(kind)
result.add(">")
proc `$`*(n: VNode): kstring =
when defined(js):
var res = ""
toString(n, res, 0)
result = kstring(res)
else:
result = ""
add(result, n)
proc getVNodeById*(n: VNode; id: cstring): VNode =
## Get the VNode that was marked with ``id``. Returns ``nil``
## if no node exists.
if n.id == id: return n
for i in 0..<n.len:
result = getVNodeById(n[i], id)
if result != nil: return result

View file

@ -1,354 +0,0 @@
##[
see examples/hellostyle.nim
]##
import std/[macros, strutils]
import kbase
when defined(js):
import kdom, jdict
type
StyleAttr* {.pure.} = enum
## The style attributes supported by the virtual DOM.
## Reference: https://www.w3schools.com/jsref/dom_obj_style.asp
alignContent
alignItems
alignSelf
animation
animationDelay
animationDirection
animationDuration
animationFillMode
animationIterationCount
animationName
animationTimingFunction
animationPlayState
background
backgroundAttachment
backgroundColor
backgroundImage
backgroundPosition
backgroundRepeat
backgroundClip
backgroundOrigin
backgroundSize
backfaceVisibility
border
borderBottom
borderBottomColor
borderBottomLeftRadius
borderBottomRightRadius
borderBottomStyle
borderBottomWidth
borderCollapse
borderColor
borderImage
borderImageOutset
borderImageRepeat
borderImageSlice
borderImageSource
borderImageWidth
borderLeft
borderLeftColor
borderLeftStyle
borderLeftWidth
borderRadius
borderRight
borderRightColor
borderRightStyle
borderRightWidth
borderSpacing
borderStyle
borderTop
borderTopColor
borderTopLeftRadius
borderTopRightRadius
borderTopStyle
borderTopWidth
borderWidth
bottom
boxDecorationBreak
boxShadow
boxSizing
captionSide
clear
clip
color
columnCount
columnFill
columnGap
columnRule
columnRuleColor
columnRuleStyle
columnRuleWidth
columns
columnSpan
columnWidth
content
counterIncrement
counterReset
cursor
direction
display
emptyCells
filter
flex
flexBasis
flexDirection
flexFlow
flexGrow
flexShrink
flexWrap
cssFloat
font
fontFamily
fontSize
fontSizeAdjust
fontStretch
fontStyle
fontVariant
fontWeight
hangingPunctuation
height
hyphens
icon
imageOrientation
justifyContent
left
letterSpacing
lineHeight
listStyle
listStyleImage
listStylePosition
listStyleType
margin
marginBottom
marginLeft
marginRight
marginTop
maxHeight
maxWidth
minHeight
minWidth
navDown
navIndex
navLeft
navRight
navUp
opacity
order
orphans
outline
outlineColor
outlineOffset
outlineStyle
outlineWidth
overflow
overflowX
overflowY
padding
paddingBottom
paddingLeft
paddingRight
paddingTop
pageBreakAfter
pageBreakBefore
pageBreakInside
perspective
perspectiveOrigin
pointerEvents
position
quotes
resize
right
scrollbar3dLightColor # note: missing in w3schools reference
scrollbarArrowColor # note: missing in w3schools reference
scrollbarBaseColor # note: missing in w3schools reference
scrollbarDarkshadowColor # note: missing in w3schools reference
scrollbarFaceColor # note: missing in w3schools reference
scrollbarHighlightColor # note: missing in w3schools reference
scrollbarShadowColor # note: missing in w3schools reference
scrollbarTrackColor # note: missing in w3schools reference
tableLayout
tabSize
textAlign
textAlignLast
textDecoration
textDecorationColor
textDecorationLine
textDecorationStyle
textIndent
textJustify
textOverflow
textShadow
textTransform
top
transform
transformOrigin
transformStyle
transition
transitionDelay
transitionDuration
transitionProperty
transitionTimingFunction
unicodeBidi
userSelect
verticalAlign
visibility
whiteSpace
width
wordBreak
wordSpacing
wordWrap
widows
zIndex
macro buildLookupTables(): untyped =
var e = newTree(nnkBracket)
for i in low(StyleAttr)..high(StyleAttr):
e.add(newCall("kstring", newLit($i)))
template tmpl(e) {.dirty.} =
const
toStyleAttrName: array[StyleAttr, kstring] = e
result = getAst tmpl(e)
buildLookupTables()
# I optimized the heck out of this representation since profiling showed
# it's relevant.
# even index: key, odd index: value; done this way for memory efficiency:
when defined(js):
type
VStyle* = JSeq[cstring]
else:
type
VStyle* = ref seq[string]
proc len*(a: VStyle): int = len(a[])
proc add*(a: VStyle; x: string) = add(a[], x)
proc eq*(a, b: VStyle): bool =
if a.isNil:
if b.isNil: return true
else: return false
elif b.isNil: return false
if a.len != b.len: return false
for i in 0..<a.len:
if a[i] != b[i]: return false
return true
proc setAttr*(s: VStyle; a, value: kstring) {.noSideEffect.} =
## inserts (a, value) in sorted order of key `a`
# worst case quadratic complexity (if given styles in reverse order), hopefully
# not a concern assuming small cardinal
var i = 0
while i < s.len:
if s[i] == a:
s[i+1] = value
return
elif s[i] > a:
s.add ""
s.add ""
# insertion point here, shift all remaining pairs by 2 indexes
for j in countdown(s.len-1, i+3, 2):
s[j] = s[j-2]
s[j-1] = s[j-3]
s[i] = a
s[i+1] = value
return
inc i, 2
s.add a
s.add value
proc setAttr*(s: VStyle; attr: StyleAttr, value: kstring) {.noSideEffect.} =
when kstring is cstring:
assert value != nil, "value must not be nil"
setAttr(s, toStyleAttrName[attr], value)
proc getAttr*(s: VStyle; attr: StyleAttr): kstring {.noSideEffect.} =
## returns "" if the attribute has not been set.
var i = 0
let a = toStyleAttrName[attr]
while i < s.len:
if s[i] == a:
return s[i+1]
elif s[i] > a:
return ""
inc i, 2
proc style*(pairs: varargs[(StyleAttr, kstring)]): VStyle {.noSideEffect.} =
## constructs a VStyle object from a list of (attribute, value)-pairs.
when defined(js):
result = newJSeq[cstring]()
else:
new(result)
result[] = @[]
for x in pairs:
result.setAttr x[0], x[1]
proc style*(a: StyleAttr; val: kstring): VStyle {.noSideEffect.} =
## constructs a VStyle object from a single (attribute, value)-pair.
when defined(js):
result = newJSeq[cstring]()
else:
new(result)
result[] = @[]
result.setAttr a, val
proc toCss*(a: string): VStyle =
##[
See example in hellostyle.nim
Allows passing a css string directly, eg:
tdiv(style = style((fontStyle, "italic".kstring), (color, "orange".kstring))): discard
tdiv(style = "font-style: oblique; color: pink".toCss): discard
]##
when defined(js):
result = newJSeq[cstring]()
else:
new(result)
result[] = @[]
for ai in a.split(";"):
var ai = ai.strip
if ai.len == 0: continue
let aj = ai.strip.split(":", maxsplit=1)
result.setAttr(aj[0], aj[1])
when defined(js):
proc setStyle(d: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.}
proc applyStyle*(n: Node; s: VStyle) {.noSideEffect.} =
## apply the style to the real DOM node ``n``.
#n.style = Style() # optimized, this is a hotspot:
{.emit: "`n`.style = {};".}
for i in countup(0, s.len-1, 2):
n.style.setStyle(s[i], s[i+1])
proc merge*(a, b: VStyle): VStyle {.noSideEffect.} =
## merges two styles. ``b`` takes precedence over ``a``.
when defined(js):
result = newJSeq[cstring]()
else:
new(result)
result[] = @[]
for i in 0..<a.len:
result.add a[i]
for i in countup(0, b.len-1, 2):
setAttr(result, b[i], b[i+1])
iterator pairs*(s: VStyle): (kstring, kstring) {.noSideEffect.} =
if s != nil:
for i in countup(0, s.len-1, 2):
yield (s[i], s[i+1])
when defined(js):
import jstrutils
else:
template `&`(x: untyped): untyped = $x
proc rgb*(r, g, b: range[0..255]): kstring =
kstring"rgb(" & &r & kstring", " & &g & kstring", " & &b & kstring")"
proc rgba*(r, g, b: range[0..255], alpha: float): kstring =
kstring"rgba(" & &r & kstring", " & &g & kstring", " & &b & kstring", " & &alpha & kstring")"

394
readme.md
View file

@ -1,394 +0,0 @@
![karax](https://user-images.githubusercontent.com/22755228/117183486-482b2a00-ade0-11eb-88e6-d8eeb28951ca.png)
![Github Actions](https://img.shields.io/github/workflow/status/karaxnim/karax/CI?style=for-the-badge) ![GitHub issues](https://img.shields.io/github/issues-raw/karaxnim/karax?style=for-the-badge) ![GitHub](https://img.shields.io/github/license/karaxnim/karax?style=for-the-badge) ![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/karaxnim/karax?sort=semver&style=for-the-badge) ![https://nim-lang.org](https://img.shields.io/badge/nim-powered-ffc200?style=for-the-badge)
# Karax
Karax is a framework for developing single page applications in Nim.
## Install
To use Karax you must have nim installed. You can follow the instructions [here](https://nim-lang.org/install.html).
Then you can install karax through nimble:
``nimble install karax``
## Try Karax
To try it out, run:
``cd ~/projects # Insert your favourite directory for projects``
``nimble develop karax # This will clone Karax and create a link to it in ~/.nimble``
``cd karax``
``cd examples/todoapp``
``nim js todoapp.nim``
``open todoapp.html``
``cd ../..``
``cd examples/mediaplayer``
``nim js playerapp.nim``
``open playerapp.html``
It uses a virtual DOM like React, but is much smaller than the existing
frameworks plus of course it's written in Nim for Nim. No external
dependencies! And thanks to Nim's whole program optimization only what
is used ends up in the generated JavaScript code.
## Goals
- Leverage Nim's macro system to produce a framework that allows
for the development of applications that are boilerplate free.
- Keep it small, keep it fast, keep it flexible.
## Hello World
The simplest Karax program looks like this:
```nim
include karax / prelude
proc createDom(): VNode =
result = buildHtml(tdiv):
text "Hello World!"
setRenderer createDom
```
Since ``div`` is a keyword in Nim, karax choose to use ``tdiv`` instead
here. ``tdiv`` produces a ``<div>`` virtual DOM node.
As you can see, karax comes with its own ``buildHtml`` DSL for convenient
construction of (virtual) DOM trees (of type ``VNode``). Karax provides
a tiny build tool called ``karun`` that generates the HTML boilerplate code that
embeds and invokes the generated JavaScript code::
``nim c karax/tools/karun``
``karax/tools/karun -r helloworld.nim``
Via ``-d:debugKaraxDsl`` we can have a look at the produced Nim code by
``buildHtml``:
```nim
let tmp1 = tree(VNodeKind.tdiv)
add(tmp1, text "Hello World!")
tmp1
```
(I shortened the IDs for better readability.)
Ok, so ``buildHtml`` introduces temporaries and calls ``add`` for the tree
construction so that it composes with all of Nim's control flow constructs:
```nim
include karax / prelude
import random
proc createDom(): VNode =
result = buildHtml(tdiv):
if rand(100) <= 50:
text "Hello World!"
else:
text "Hello Universe"
randomize()
setRenderer createDom
```
Produces:
```nim
let tmp1 = tree(VNodeKind.tdiv)
if rand(100) <= 50:
add(tmp1, text "Hello World!")
else:
add(tmp1, text "Hello Universe")
tmp1
```
## Event model
Karax does not change the DOM's event model much, here is a program
that writes "Hello simulated universe" on a button click:
```nim
include karax / prelude
# alternatively: import karax / [kbase, vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, jjson]
var lines: seq[kstring] = @[]
proc createDom(): VNode =
result = buildHtml(tdiv):
button:
text "Say hello!"
proc onclick(ev: Event; n: VNode) =
lines.add "Hello simulated universe"
for x in lines:
tdiv:
text x
setRenderer createDom
```
``kstring`` is Karax's alias for ``cstring`` (which stands for "compatible
string"; for the JS target that is an immutable JavaScript string) which
is preferred for efficiency on the JS target. However, on the native targets
``kstring`` is mapped to ``string`` for efficiency. The DSL for HTML
construction is also avaible for the native targets (!) and the ``kstring``
abstraction helps to deal with these conflicting requirements.
Karax's DSL is quite flexible when it comes to event handlers, so the
following syntax is also supported:
```nim
include karax / prelude
from sugar import `=>`
var lines: seq[kstring] = @[]
proc createDom(): VNode =
result = buildHtml(tdiv):
button(onclick = () => lines.add "Hello simulated universe"):
text "Say hello!"
for x in lines:
tdiv:
text x
setRenderer createDom
```
The ``buildHtml`` macro produces this code for us:
```nim
let tmp2 = tree(VNodeKind.tdiv)
let tmp3 = tree(VNodeKind.button)
addEventHandler(tmp3, EventKind.onclick,
() => lines.add "Hello simulated universe", kxi)
add(tmp3, text "Say hello!")
add(tmp2, tmp3)
for x in lines:
let tmp4 = tree(VNodeKind.tdiv)
add(tmp4, text x)
add(tmp2, tmp4)
tmp2
```
As the examples grow larger it becomes more and more visible of what
a DSL that composes with the builtin Nim control flow constructs buys us.
Once you have tasted this power there is no going back and languages
without AST based macro system simply don't cut it anymore.
## Attaching data to an event handler
Since the type of an event handler is ``(ev: Event; n: VNode)`` or ``()`` any
additional data that should be passed to the event handler needs to be
done via Nim's closures. In general this means a pattern like this:
```nim
proc menuAction(menuEntry: kstring): proc() =
result = proc() =
echo "clicked ", menuEntry
proc buildMenu(menu: seq[kstring]): VNode =
result = buildHtml(tdiv):
for m in menu:
nav(class="navbar is-primary"):
tdiv(class="navbar-brand"):
a(class="navbar-item", onclick = menuAction(m)):
```
## DOM diffing
Ok, so now we have seen DOM creation and event handlers. But how does
Karax actually keep the DOM up to date? The trick is that every event
handler is wrapped in a helper proc that triggers a *redraw* operation
that calls the *renderer* that you initially passed to ``setRenderer``.
So a new virtual DOM is created and compared against the previous
virtual DOM. This comparison produces a patch set that is then applied
to the real DOM the browser uses internally. This process is called
"virtual DOM diffing" and other frameworks, most notably Facebook's
*React*, do quite similar things. The virtual DOM is faster to create
and manipulate than the real DOM so this approach is quite efficient.
## Form validation
Most applications these days have some "login"
mechanism consisting of ``username`` and ``password`` and
a ``login`` button. The login button should only be clickable
if ``username`` and ``password`` are not empty. An error
message should be shown as long as one input field is empty.
To create new UI elements we write a ``loginField`` proc that
returns a ``VNode``:
```nim
proc loginField(desc, field, class: kstring;
validator: proc (field: kstring): proc ()): VNode =
result = buildHtml(tdiv):
label(`for` = field):
text desc
input(class = class, id = field, onchange = validator(field))
```
We use the ``karax / errors`` module to help with this error
logic. The ``errors`` module is mostly a mapping from strings to
strings but it turned out that the logic is tricky enough to warrant
a library solution. ``validateNotEmpty`` returns a closure that
captures the ``field`` parameter:
```nim
proc validateNotEmpty(field: kstring): proc () =
result = proc () =
let x = getVNodeById(field).getInputText
if x.isNil or x == "":
errors.setError(field, field & " must not be empty")
else:
errors.setError(field, "")
```
This indirection is required because
event handlers in Karax need to have the type ``proc ()``
or ``proc (ev: Event; n: VNode)``. The errors module also
gives us a handy ``disableOnError`` helper. It returns
``"disabled"`` if there are errors. Now we have all the
pieces together to write our login dialog:
```nim
# some consts in order to prevent typos:
const
username = kstring"username"
password = kstring"password"
var loggedIn: bool
proc loginDialog(): VNode =
result = buildHtml(tdiv):
if not loggedIn:
loginField("Name :", username, "input", validateNotEmpty)
loginField("Password: ", password, "password", validateNotEmpty)
button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()):
text "Login"
p:
text errors.getError(username)
p:
text errors.getError(password)
else:
p:
text "You are now logged in."
setRenderer loginDialog
```
(Full example [here](https://github.com/karaxnim/karax/blob/master/examples/login.nim).)
This code still has a bug though, when you run it, the ``login`` button is not
disabled until some input fields are validated! This is easily fixed,
at initialization we have to do:
```nim
setError username, username & " must not be empty"
setError password, password & " must not be empty"
```
There are likely more elegant solutions to this problem.
## Routing
For routing ``setRenderer`` can be called with a callback that takes a parameter of
type ``RouterData``. Here is the relevant excerpt from the famous "Todo App" example:
```nim
proc createDom(data: RouterData): VNode =
if data.hashPart == "#/": filter = all
elif data.hashPart == "#/completed": filter = completed
elif data.hashPart == "#/active": filter = active
result = buildHtml(tdiv(class="todomvc-wrapper")):
section(class = "todoapp"):
...
setRenderer createDom
```
(Full example [here](https://github.com/karaxnim/karax/blob/master/examples/todoapp/todoapp.nim).)
## Server Side HTML Rendering
Karax can also be used to render HTML on the server. Only a subset of
modules can be used since there is no JS interpreter.
```nim
import karax / [karaxdsl, vdom]
const places = @["boston", "cleveland", "los angeles", "new orleans"]
proc render*(): string =
let vnode = buildHtml(tdiv(class = "mt-3")):
h1: text "My Web Page"
p: text "Hello world"
ul:
for place in places:
li: text place
dl:
dt: text "Can I use Karax for client side single page apps?"
dd: text "Yes"
dt: text "Can I use Karax for server side HTML rendering?"
dd: text "Yes"
result = $vnode
echo render()
```
## Generate HTML with event handlers
If you are writing a static site generator or do server-side HTML rendering
via ``nim c``, you may want to override ``addEventHandler`` when using event
handlers to avoid compiler complaints.
Here's an example of auto submit a dropdown when a value is selected:
```nim
template kxi(): int = 0
template addEventHandler(n: VNode; k: EventKind; action: string; kxi: int) =
n.setAttr($k, action)
let
names = @["nim", "c", "python"]
selected_name = request.params.getOrDefault("name")
hello = buildHtml(html):
form(`method` = "get"):
select(name="name", onchange="this.form.submit()"):
for name in names:
if name == selected_name:
option(selected = ""): text name
else:
option: text name
```
## License
MIT License. See [here](https://github.com/karaxnim/karax/blob/master/LICENSE.txt).

35
readme.rst Normal file
View file

@ -0,0 +1,35 @@
Karax -- Single page applications in Nim
========================================
Karax is a framework for developing single page applications in Nim.
It's still in heavy development, so keep in mind that the API is subject
to change.
To try it out, run::
nimble install # For first time
cd examples/todoapp
nim js todoapp.nim
open todoapp.html
cd ../..
cd examples/mediaplayer
nim js playerapp.nim
open playerapp.html
It uses a virtual DOM like React, but is much smaller than the existing
frameworks plus of course it's written in Nim for Nim. No external
dependencies! And thanks to Nim's whole program optimization only what
is used ends up in the generated JavaScript code. But don't take my
word for it, look at this:
.. image:: docs/benchmark.png
Goals
=====
- Leverage Nim's macro system to produce a framework that allows
for the development of applications that are boilerplate free.
- Keep it small, keep it fast, keep it flexible.

225
src/components.nim Normal file
View file

@ -0,0 +1,225 @@
## Components in Karax are built by the ``.component`` macro annotation.
import macros, jdict, dom, vdom, tables, strutils
type
StateDict*[V] = ref object
defaultValue*: V
proc get[V](d: StateDict[V], k: VKey): V {.importcpp: "#[#]".}
proc put[V](d: StateDict[V], k: VKey, v: V) {.importcpp: "#[#] = #".}
proc contains*[V](d: StateDict[V], k: VKey): bool {.importcpp: "#.hasOwnProperty(#)".}
proc del*[V](d: StateDict[V], k: VKey) {.importcpp: "delete #[#]".}
proc newStateDict*[V](): StateDict[V] {.importcpp: "{@}".}
var
dirty = newStateDict[bool]()
someDirty*: bool
proc markDirty*(key: VKey) =
dirty.put(key, true)
someDirty = true
proc unmarkDirty*(key: VKey) = dirty.del key
proc isDirty*(key: VKey): bool = dirty.contains(key)
proc `[]`*[V](d: StateDict[V], k: VKey): V =
if d.contains(k): result = d.get(k)
else: result = d.defaultValue
proc `[]=`*[V](d: StateDict[V], k: VKey, v: V) =
d.put(k, v)
markDirty(k)
var
vcomponents* = newJDict[cstring, proc(args: seq[VNode]): VNode]()
dcomponents* = newJDict[cstring, proc(args: seq[VNode]): Node]()
type
ComponentKind* {.pure.} = enum
None,
Tag,
VNode,
Node
var
allcomponents {.compileTime.} = initTable[string, ComponentKind]()
proc isComponent*(x: string): ComponentKind {.compileTime.} =
allcomponents.getOrDefault(x)
proc addTags() {.compileTime.} =
let x = (bindSym"VNodeKind").getTypeImpl
expectKind(x, nnkEnumTy)
for i in ord(VNodeKind.html)..ord(VNodeKind.high):
# +1 because of empty node at the start of the enum AST:
let tag = $x[i+1]
allcomponents[tag] = ComponentKind.Tag
static:
addTags()
template toState(x): untyped = newIdentNode("state" & x)
proc accessState(sv: NimNode): NimNode {.compileTime.} =
newTree(nnkBracketExpr, sv, newIdentNode("key"))
proc stateDecl(n: NimNode; names: TableRef[string, bool]; decl: NimNode) =
case n.kind
of nnkVarSection, nnkLetSection:
for c in n:
expectKind c, nnkIdentDefs
let typ = c[^2]
let val = c[^1]
let usedType = if typ.kind != nnkEmpty: typ else: newCall("type", val)
if usedType.kind == nnkEmpty:
error("cannot determine the variable's type", c)
for i in 0 .. c.len-3:
let v = $c[i]
let sv = toState v
decl.add quote do:
var `sv` = newStateDict[`usedType`]()
if val.kind != nnkEmpty:
decl.add newTree(nnkAsgn, newDotExpr(sv, newIdentNode"defaultValue"),
val)
else:
error("component state must have a primitive initializer")
names[v] = true
of nnkStmtList, nnkStmtListExpr:
for x in n: stateDecl(x, names, decl)
of nnkDo:
stateDecl(n.body, names, decl)
of nnkCommentStmt: discard
else:
error("invalid 'state' declaration", n)
proc doState(n: NimNode; names: TableRef[string, bool];
decl: NimNode): NimNode =
case n.kind
of nnkCallKinds:
# handle 'state' declaration and remove it from the AST:
if n.len == 2 and repr(n[0]) == "state":
stateDecl(n[1], names, decl)
return newTree(nnkNone)
of nnkSym, nnkIdent:
let v = $n
if v in names:
let sv = toState v
return accessState(sv)
else: discard
result = copyNimNode(n)
for i in 0..<n.len:
let x = doState(n[i], names, decl)
if x.kind != nnkNone: result.add x
proc compBody(body, decl: NimNode): NimNode =
var names = newTable[string, bool]()
result = doState(body, names, decl)
proc unpack(symbolicType: NimNode; index: int): NimNode {.compileTime.} =
#let t = symbolicType.getTypeImpl
let t = repr(symbolicType)
case t
of "cstring":
result = quote do:
args[`index`].text
of "int", "VKey":
result = quote do:
args[`index`].intValue
of "bool":
result = quote do:
args[`index`].intValue != 0
elif t.endsWith"Kind":
result = quote do:
`symbolicType`(args[`index`].intValue)
else:
# just pass it along, maybe there is some conversion for it:
result = quote do:
args[`index`]
proc newname*(n: NimNode): NimNode =
if n.kind == nnkPostfix:
n[1] = newname(n[1])
result = n
elif n.kind == nnkSym:
result = ident($n.symbol)
else:
result = n
macro component*(prc: untyped): untyped =
## A component takes an proc body and registers it as a component to the
## virtual dom.
var n = prc.copyNimNode
for i in 0..6: n.add prc[i].copyNimTree
expectKind(n, nnkProcDef)
if n[0].kind == nnkEmpty:
error("please pass a non anonymous proc", n[0])
let name = n[0]
let params = params(n)
let rettype = repr params[0]
var isvirtual = ComponentKind.None
if rettype == "VNode":
isvirtual = ComponentKind.VNode
elif rettype == "Node":
isvirtual = ComponentKind.Node
else:
error "component must return VNode or Node", params[0]
let realName = if name.kind == nnkPostfix: name[1] else: name
let nn = $realName
n[0] = ident("inner" & nn)
var unpackCall = newCall(n[0])
var counter = 0
for i in 1.. <params.len:
let param = params[i]
let L = param.len
let typ = param[L-2]
for j in 0 .. L-3:
unpackCall.add unpack(typ, counter)
inc counter
let decl = newTree(nnkStmtList)
let newBody = compBody(n, decl)
template vwrapper(pname, unpackCall) {.dirty.} =
proc pname(args: seq[VNode]): VNode =
unpackCall
template dwrapper(pname, unpackCall) {.dirty.} =
proc pname(args: seq[VNode]): Node =
unpackCall
template vregister(key, val) =
bind jdict.`[]=`
`[]=`(vcomponents, cstring(key), val)
template dregister(key, val) =
bind jdict.`[]=`
`[]=`(dcomponents, cstring(key), val)
result = newTree(nnkStmtList, decl, newBody)
if isvirtual == ComponentKind.VNode:
result.add getAst(vwrapper(newname name, unpackCall))
result.add getAst(vregister(newLit(nn), realName))
else:
result.add getAst(dwrapper(newname name, unpackCall))
result.add getAst(dregister(newLit(nn), realName))
allcomponents[nn] = isvirtual
when defined(debugKaraxDsl):
echo repr result
when isMainModule:
proc public*(key: VKey; x, y: int, b: bool; s: cstring): VNode {.component.} =
state:
var foo = 89
proc callback() =
foo = 78
let cc = callback
when false:
proc private(x, y: int, b: bool; s: cstring): VNode {.component.} =
discard

586
src/dom.nim Normal file
View file

@ -0,0 +1,586 @@
#
#
# Nim's Runtime Library
# (c) Copyright 2012 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## Declaration of the Document Object Model for the `JavaScript backend
## <backends.html#the-javascript-target>`_.
when not defined(js) and not defined(Nimdoc):
{.error: "This module only works on the JavaScript platform".}
type
EventTarget* = ref EventTargetObj
EventTargetObj {.importc.} = object of RootObj
onabort*: proc (event: Event) {.nimcall.}
onblur*: proc (event: Event) {.nimcall.}
onchange*: proc (event: Event) {.nimcall.}
onclick*: proc (event: Event) {.nimcall.}
ondblclick*: proc (event: Event) {.nimcall.}
onerror*: proc (event: Event) {.nimcall.}
onfocus*: proc (event: Event) {.nimcall.}
onkeydown*: proc (event: Event) {.nimcall.}
onkeypress*: proc (event: Event) {.nimcall.}
onkeyup*: proc (event: Event) {.nimcall.}
onload*: proc (event: Event) {.nimcall.}
onmousedown*: proc (event: Event) {.nimcall.}
onmousemove*: proc (event: Event) {.nimcall.}
onmouseout*: proc (event: Event) {.nimcall.}
onmouseover*: proc (event: Event) {.nimcall.}
onmouseup*: proc (event: Event) {.nimcall.}
onreset*: proc (event: Event) {.nimcall.}
onselect*: proc (event: Event) {.nimcall.}
onsubmit*: proc (event: Event) {.nimcall.}
onunload*: proc (event: Event) {.nimcall.}
Window* = ref WindowObj
WindowObj {.importc.} = object of EventTargetObj
document*: Document
event*: Event
history*: History
location*: Location
closed*: bool
defaultStatus*: cstring
innerHeight*, innerWidth*: int
locationbar*: ref LocationBar
menubar*: ref MenuBar
name*: cstring
outerHeight*, outerWidth*: int
pageXOffset*, pageYOffset*: int
personalbar*: ref PersonalBar
scrollbars*: ref ScrollBars
statusbar*: ref StatusBar
status*: cstring
toolbar*: ref ToolBar
frames*: seq[Frame]
Frame* = ref FrameObj
FrameObj {.importc.} = object of WindowObj
ClassList* = ref ClassListObj
ClassListObj {.importc.} = object of RootObj
NodeType* = enum
ElementNode = 1,
AttributeNode,
TextNode,
CDATANode,
EntityRefNode,
EntityNode,
ProcessingInstructionNode,
CommentNode,
DocumentNode,
DocumentTypeNode,
DocumentFragmentNode,
NotationNode
Node* = ref NodeObj
NodeObj {.importc.} = object of EventTargetObj
attributes*: seq[Node]
childNodes*: seq[Node]
children*: seq[Node]
data*: cstring
firstChild*: Node
lastChild*: Node
nextSibling*: Node
nodeName*: cstring
nodeType*: NodeType
nodeValue*: cstring
parentNode*: Node
previousSibling*: Node
innerHTML*: cstring
style*: Style
Document* = ref DocumentObj
DocumentObj {.importc.} = object of NodeObj
alinkColor*: cstring
bgColor*: cstring
body*: Element
charset*: cstring
cookie*: cstring
defaultCharset*: cstring
fgColor*: cstring
head*: Element
lastModified*: cstring
linkColor*: cstring
referrer*: cstring
title*: cstring
URL*: cstring
vlinkColor*: cstring
anchors*: seq[AnchorElement]
forms*: seq[FormElement]
images*: seq[ImageElement]
applets*: seq[Element]
embeds*: seq[EmbedElement]
links*: seq[LinkElement]
Element* = ref ElementObj
ElementObj {.importc.} = object of NodeObj
classList*: Classlist
checked*: bool
defaultChecked*: bool
defaultValue*: cstring
disabled*: bool
form*: FormElement
name*: cstring
readOnly*: bool
options*: seq[OptionElement]
# https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement
HtmlElement* = ref object of Element
contentEditable*: string
isContentEditable*: bool
dir*: string
offsetHeight*: int
offsetWidth*: int
offsetLeft*: int
offsetTop*: int
LinkElement* = ref LinkObj
LinkObj {.importc.} = object of ElementObj
target*: cstring
text*: cstring
x*: int
y*: int
EmbedElement* = ref EmbedObj
EmbedObj {.importc.} = object of ElementObj
height*: int
hspace*: int
src*: cstring
width*: int
`type`*: cstring
vspace*: int
AnchorElement* = ref AnchorObj
AnchorObj {.importc.} = object of ElementObj
text*: cstring
x*, y*: int
OptionElement* = ref OptionObj
OptionObj {.importc.} = object of ElementObj
defaultSelected*: bool
selected*: bool
selectedIndex*: int
text*: cstring
value*: cstring
FormElement* = ref FormObj
FormObj {.importc.} = object of ElementObj
action*: cstring
encoding*: cstring
`method`*: cstring
target*: cstring
elements*: seq[Element]
ImageElement* = ref ImageObj
ImageObj {.importc.} = object of ElementObj
border*: int
complete*: bool
height*: int
hspace*: int
lowsrc*: cstring
src*: cstring
vspace*: int
width*: int
Style = ref StyleObj
StyleObj {.importc.} = object of RootObj
background*: cstring
backgroundAttachment*: cstring
backgroundColor*: cstring
backgroundImage*: cstring
backgroundPosition*: cstring
backgroundRepeat*: cstring
border*: cstring
borderBottom*: cstring
borderBottomColor*: cstring
borderBottomStyle*: cstring
borderBottomWidth*: cstring
borderColor*: cstring
borderLeft*: cstring
borderLeftColor*: cstring
borderLeftStyle*: cstring
borderLeftWidth*: cstring
borderRight*: cstring
borderRightColor*: cstring
borderRightStyle*: cstring
borderRightWidth*: cstring
borderStyle*: cstring
borderTop*: cstring
borderTopColor*: cstring
borderTopStyle*: cstring
borderTopWidth*: cstring
borderWidth*: cstring
bottom*: cstring
captionSide*: cstring
clear*: cstring
clip*: cstring
color*: cstring
cursor*: cstring
direction*: cstring
display*: cstring
emptyCells*: cstring
cssFloat*: cstring
font*: cstring
fontFamily*: cstring
fontSize*: cstring
fontStretch*: cstring
fontStyle*: cstring
fontVariant*: cstring
fontWeight*: cstring
height*: cstring
left*: cstring
letterSpacing*: cstring
lineHeight*: cstring
listStyle*: cstring
listStyleImage*: cstring
listStylePosition*: cstring
listStyleType*: cstring
margin*: cstring
marginBottom*: cstring
marginLeft*: cstring
marginRight*: cstring
marginTop*: cstring
maxHeight*: cstring
maxWidth*: cstring
minHeight*: cstring
minWidth*: cstring
overflow*: cstring
padding*: cstring
paddingBottom*: cstring
paddingLeft*: cstring
paddingRight*: cstring
paddingTop*: cstring
pageBreakAfter*: cstring
pageBreakBefore*: cstring
position*: cstring
right*: cstring
scrollbar3dLightColor*: cstring
scrollbarArrowColor*: cstring
scrollbarBaseColor*: cstring
scrollbarDarkshadowColor*: cstring
scrollbarFaceColor*: cstring
scrollbarHighlightColor*: cstring
scrollbarShadowColor*: cstring
scrollbarTrackColor*: cstring
tableLayout*: cstring
textAlign*: cstring
textDecoration*: cstring
textIndent*: cstring
textTransform*: cstring
top*: cstring
verticalAlign*: cstring
visibility*: cstring
width*: cstring
wordSpacing*: cstring
zIndex*: int
# TODO: A lot of the fields in Event belong to a more specific type of event.
# TODO: Should we clean this up?
Event* = ref EventObj
EventObj {.importc.} = object of RootObj
target*: Node
altKey*, ctrlKey*, shiftKey*: bool
button*: int
clientX*, clientY*: int
keyCode*: int
layerX*, layerY*: int
modifiers*: int
ALT_MASK*, CONTROL_MASK*, SHIFT_MASK*, META_MASK*: int
offsetX*, offsetY*: int
pageX*, pageY*: int
screenX*, screenY*: int
which*: int
`type`*: cstring
x*, y*: int
ABORT*: int
BLUR*: int
CHANGE*: int
CLICK*: int
DBLCLICK*: int
DRAGDROP*: int
ERROR*: int
FOCUS*: int
KEYDOWN*: int
KEYPRESS*: int
KEYUP*: int
LOAD*: int
MOUSEDOWN*: int
MOUSEMOVE*: int
MOUSEOUT*: int
MOUSEOVER*: int
MOUSEUP*: int
MOVE*: int
RESET*: int
RESIZE*: int
SELECT*: int
SUBMIT*: int
UNLOAD*: int
TouchList* {.importc.} = ref object of RootObj
length*: int
TouchEvent* {.importc.} = ref object of Event
changedTouches*, targetTouches*, touches*: TouchList
Touch* {.importc.} = ref object of RootObj
identifier*: int
screenX*, screenY*, clientX*, clientY*, pageX*, pageY*: int
target*: Element
radiusX*, radiusY*: int
rotationAngle*: int
force*: float
Location* = ref LocationObj
LocationObj {.importc.} = object of RootObj
hash*: cstring
host*: cstring
hostname*: cstring
href*: cstring
pathname*: cstring
port*: cstring
protocol*: cstring
search*: cstring
History* = ref HistoryObj
HistoryObj {.importc.} = object of RootObj
length*: int
Navigator* = ref NavigatorObj
NavigatorObj {.importc.} = object of RootObj
appCodeName*: cstring
appName*: cstring
appVersion*: cstring
cookieEnabled*: bool
language*: cstring
platform*: cstring
userAgent*: cstring
mimeTypes*: seq[ref MimeType]
Plugin* {.importc.} = object of RootObj
description*: cstring
filename*: cstring
name*: cstring
MimeType* {.importc.} = object of RootObj
description*: cstring
enabledPlugin*: ref Plugin
suffixes*: seq[cstring]
`type`*: cstring
LocationBar* {.importc.} = object of RootObj
visible*: bool
MenuBar* = LocationBar
PersonalBar* = LocationBar
ScrollBars* = LocationBar
ToolBar* = LocationBar
StatusBar* = LocationBar
Screen = ref ScreenObj
ScreenObj {.importc.} = object of RootObj
availHeight*: int
availWidth*: int
colorDepth*: int
height*: int
pixelDepth*: int
width*: int
TimeOut* {.importc.} = ref object of RootObj
Interval* {.importc.} = object of RootObj
proc len*(x: Node): int {.importcpp: "#.childNodes.length".}
proc `[]`*(x: Node; idx: int): Element {.importcpp: "#.childNodes[#]".}
proc setTimeout*(action: proc(); ms: int): Timeout {.importc, nodecl.}
proc clearTimeout*(t: Timeout) {.importc, nodecl.}
proc getElementById*(id: cstring): Element {.importc: "document.getElementById", nodecl.}
{.push importcpp.}
# EventTarget "methods"
proc addEventListener*(et: EventTarget, ev: cstring, cb: proc(ev: Event), useCapture: bool = false)
# Window "methods"
proc alert*(w: Window, msg: cstring)
proc back*(w: Window)
proc blur*(w: Window)
proc captureEvents*(w: Window, eventMask: int) {.deprecated.}
proc clearInterval*(w: Window, interval: ref Interval)
proc clearTimeout*(w: Window, timeout: TimeOut)
proc close*(w: Window)
proc confirm*(w: Window, msg: cstring): bool
proc disableExternalCapture*(w: Window)
proc enableExternalCapture*(w: Window)
proc find*(w: Window, text: cstring, caseSensitive = false,
backwards = false)
proc focus*(w: Window)
proc forward*(w: Window)
proc handleEvent*(w: Window, e: Event)
proc home*(w: Window)
proc moveBy*(w: Window, x, y: int)
proc moveTo*(w: Window, x, y: int)
proc open*(w: Window, uri, windowname: cstring,
properties: cstring = nil): Window
proc print*(w: Window)
proc prompt*(w: Window, text, default: cstring): cstring
proc releaseEvents*(w: Window, eventMask: int) {.deprecated.}
proc resizeBy*(w: Window, x, y: int)
proc resizeTo*(w: Window, x, y: int)
proc routeEvent*(w: Window, event: Event)
proc scrollBy*(w: Window, x, y: int)
proc scrollTo*(w: Window, x, y: int)
proc setInterval*(w: Window, code: cstring, pause: int): ref Interval
proc setInterval*(w: Window, function: proc (), pause: int): ref Interval
proc setTimeout*(w: Window, code: cstring, pause: int): TimeOut
proc setTimeout*(w: Window, function: proc (), pause: int): ref Interval
proc stop*(w: Window)
proc requestAnimationFrame*(w: Window, function: proc (time: float)): int
proc cancelAnimationFrame*(w: Window, id: int)
# Node "methods"
proc appendChild*(n, child: Node)
proc appendData*(n: Node, data: cstring)
proc cloneNode*(n: Node, copyContent: bool): Node
proc deleteData*(n: Node, start, len: int)
proc getAttribute*(n: Node, attr: cstring): cstring
proc getAttributeNode*(n: Node, attr: cstring): Node
proc hasChildNodes*(n: Node): bool
proc insertBefore*(n, newNode, before: Node)
proc insertData*(n: Node, position: int, data: cstring)
proc removeAttribute*(n: Node, attr: cstring)
proc removeAttributeNode*(n, attr: Node)
proc removeChild*(n, child: Node)
proc replaceChild*(n, newNode, oldNode: Node)
proc replaceData*(n: Node, start, len: int, text: cstring)
proc scrollIntoView*(n: Node)
proc setAttribute*(n: Node, name, value: cstring)
proc setAttributeNode*(n: Node, attr: Node)
# Document "methods"
proc captureEvents*(d: Document, eventMask: int) {.deprecated.}
proc createAttribute*(d: Document, identifier: cstring): Node
proc createElement*(d: Document, identifier: cstring): Element
proc createTextNode*(d: Document, identifier: cstring): Node
proc getElementById*(d: Document, id: cstring): Element
proc getElementsByName*(d: Document, name: cstring): seq[Element]
proc getElementsByTagName*(d: Document, name: cstring): seq[Element]
proc getElementsByClassName*(d: Document, name: cstring): seq[Element]
proc getSelection*(d: Document): cstring
proc handleEvent*(d: Document, event: Event)
proc open*(d: Document)
proc releaseEvents*(d: Document, eventMask: int) {.deprecated.}
proc routeEvent*(d: Document, event: Event)
proc write*(d: Document, text: cstring)
proc writeln*(d: Document, text: cstring)
# Element "methods"
proc blur*(e: Element)
proc click*(e: Element)
proc focus*(e: Node)
proc handleEvent*(e: Element, event: Event)
proc select*(e: Element)
proc getElementsByTagName*(e: Element, name: cstring): seq[Element]
proc getElementsByClassName*(e: Element, name: cstring): seq[Element]
# FormElement "methods"
proc reset*(f: FormElement)
proc submit*(f: FormElement)
# EmbedElement "methods"
proc play*(e: EmbedElement)
proc stop*(e: EmbedElement)
# Location "methods"
proc reload*(loc: Location)
proc replace*(loc: Location, s: cstring)
# History "methods"
proc back*(h: History)
proc forward*(h: History)
proc go*(h: History, pagesToJump: int)
# Navigator "methods"
proc javaEnabled*(h: Navigator): bool
# ClassList "methods"
proc add*(c: ClassList, class: cstring)
proc remove*(c: ClassList, class: cstring)
proc contains*(c: ClassList, class: cstring):bool
proc toggle*(c: ClassList, class: cstring)
# Style "methods"
proc getAttribute*(s: Style, attr: cstring, caseSensitive=false): cstring
proc removeAttribute*(s: Style, attr: cstring, caseSensitive=false)
proc setAttribute*(s: Style, attr, value: cstring, caseSensitive=false)
# Event "methods"
proc preventDefault*(ev: Event)
# TouchEvent "methods"
proc identifiedTouch*(list: TouchList): Touch
proc item*(list: TouchList, i: int): Touch
{.pop.}
proc setAttr*(n: Node; key, val: cstring) {.importcpp: "#.setAttribute(@)".}
var
window* {.importc, nodecl.}: Window
document* {.importc, nodecl.}: Document
navigator* {.importc, nodecl.}: Navigator
screen* {.importc, nodecl.}: Screen
proc decodeURI*(uri: cstring): cstring {.importc, nodecl.}
proc encodeURI*(uri: cstring): cstring {.importc, nodecl.}
proc escape*(uri: cstring): cstring {.importc, nodecl.}
proc unescape*(uri: cstring): cstring {.importc, nodecl.}
proc decodeURIComponent*(uri: cstring): cstring {.importc, nodecl.}
proc encodeURIComponent*(uri: cstring): cstring {.importc, nodecl.}
proc isFinite*(x: BiggestFloat): bool {.importc, nodecl.}
proc isNaN*(x: BiggestFloat): bool {.importc, nodecl.}
proc parseFloat*(s: cstring): BiggestFloat {.importc, nodecl.}
proc parseInt*(s: cstring): int {.importc, nodecl.}
proc parseInt*(s: cstring, radix: int):int {.importc, nodecl.}
proc id*(n: Node): cstring {.importcpp: "#.id", nodecl.}
proc `id=`*(n: Node; x: cstring) {.importcpp: "#.id = #", nodecl.}
proc class*(n: Node): cstring {.importcpp: "#.className", nodecl.}
proc `class=`*(n: Node; v: cstring) {.importcpp: "#.className = #", nodecl.}
proc value*(n: Node): cstring {.importcpp: "#.value", nodecl.}
proc `value=`*(n: Node; v: cstring) {.importcpp: "#.value = #", nodecl.}
proc `disabled=`*(n: Node; v: bool) {.importcpp: "#.disabled = #", nodecl.}
proc getElementsByClass*(n: Node; name: cstring): seq[Node] {.
importcpp: "#.getElementsByClassName(#)", nodecl.}
type
BoundingRect* {.importc.} = object
top*, bottom*, left*, right*: int
proc getBoundingClientRect*(e: Node): BoundingRect {.
importcpp: "getBoundingClientRect", nodecl.}
proc clientHeight*(): int {.
importcpp: "(window.innerHeight || document.documentElement.clientHeight)@", nodecl}
proc clientWidth*(): int {.
importcpp: "(window.innerWidth || document.documentElement.clientWidth)@", nodecl}
proc inViewport*(el: Node): bool =
let rect = el.getBoundingClientRect()
result = rect.top >= 0 and rect.left >= 0 and
rect.bottom <= clientHeight() and
rect.right <= clientWidth()
proc scrollTop*(e: Node): int {.importcpp: "#.scrollTop", nodecl.}
proc offsetHeight*(e: Node): int {.importcpp: "#.offsetHeight", nodecl.}
proc offsetTop*(e: Node): int {.importcpp: "#.offsetTop", nodecl.}

13
src/jdict.nim Normal file
View file

@ -0,0 +1,13 @@
type
JDict*[K, V] = ref object
proc `[]`*[K, V](d: JDict[K, V], k: K): V {.importcpp: "#[#]".}
proc `[]=`*[K, V](d: JDict[K, V], k: K, v: V) {.importcpp: "#[#] = #".}
proc newJDict*[K, V](): JDict[K, V] {.importcpp: "{@}".}
proc contains*[K, V](d: JDict[K, V], k: K): bool {.importcpp: "#.hasOwnProperty(#)".}
proc del*[K, V](d: JDict[K, V], k: K) {.importcpp: "delete #[#]".}

View file

@ -4,14 +4,8 @@ proc split*(s, sep: cstring): seq[cstring] {.importcpp, nodecl.}
proc split*(s, sep: cstring; max: int): seq[cstring] {.importcpp, nodecl.}
proc strip*(s: cstring): cstring {.importcpp: "#.trim()", nodecl.}
proc startsWith*(a, b: cstring): bool {.importcpp: "startsWith", nodecl.}
proc contains*(a, b: cstring): bool {.importcpp: "(#.indexOf(#)>=0)", nodecl.}
proc containsIgnoreCase*(a, b: cstring): bool {.
importcpp: """(#.search(new RegExp(#.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$$\|]/g, "\\$$&") , "i"))>=0)""", nodecl.}
proc substr*(s: cstring; start: int): cstring {.importcpp: "substr", nodecl.}
proc substr*(s: cstring; start, length: int): cstring {.importcpp: "substr", nodecl.}
@ -20,14 +14,12 @@ proc `&`*(a, b: cstring): cstring {.importcpp: "(# + #)", nodecl.}
proc toCstr*(s: int): cstring {.importcpp: "((#)+'')", nodecl.}
proc `&`*(s: int): cstring {.importcpp: "((#)+'')", nodecl.}
proc `&`*(s: bool): cstring {.importcpp: "((#)+'')", nodecl.}
proc `&`*(s: float): cstring {.importcpp: "((#)+'')", nodecl.}
proc `&`*(s: cstring): cstring {.importcpp: "(#)", nodecl.}
proc isInt*(s: cstring): bool {.asmNoStackFrame.} =
asm """
return `s`.match(/^[0-9]+$/);
return s.match(/^[0-9]+$/);
"""
proc parseInt*(s: cstring): int {.importcpp: "parseInt(#, 10)", nodecl.}
proc parseFloat*(s: cstring): BiggestFloat {.importc, nodecl.}

50
src/kajax.nim Normal file
View file

@ -0,0 +1,50 @@
## Karax -- Single page applications for Nim.
## This module implements support for `ajax`:idx: socket
## handling.
import karax
proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)];
data: cstring;
cont: proc (httpStatus: int; response: cstring)) =
proc contWrapper(httpStatus: int; response: cstring) =
cont(httpStatus, response)
redraw()
type
HttpRequest {.importc.} = ref object
ThisObj {.importc.} = ref object
readyState, status: int
responseText, statusText: cstring
proc setRequestHeader(r: HttpRequest; a, b: cstring) {.importcpp: "#.setRequestHeader(@)".}
proc statechange(r: HttpRequest; cb: proc()) {.importcpp: "#.onreadystatechange = #".}
proc send(r: HttpRequest; data: cstring) {.importcpp: "#.send(#)".}
proc open(r: HttpRequest; meth, url: cstring; async: bool) {.importcpp: "#.open(@)".}
proc newRequest(): HttpRequest {.importcpp: "new XMLHttpRequest(@)",}
var this {.importc: "this".}: ThisObj
let ajax = newRequest()
ajax.open(meth, url, true)
for a, b in items(headers):
ajax.setRequestHeader(a, b)
ajax.statechange proc() =
if this.readyState == 4:
if this.status == 200:
contWrapper(this.status, this.responseText)
else:
contWrapper(this.status, this.statusText)
ajax.send(data)
proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)];
data: cstring;
cont: proc (httpStatus: int, response: cstring)) =
ajax("POST", url, headers, data, cont)
proc ajaxGet*(url: cstring; headers: openarray[(cstring, cstring)];
cont: proc (httpStatus: int, response: cstring)) =
ajax("GET", url, headers, nil, cont)
proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".}
proc fromJson*[T](blob: cstring): T {.importc: "JSON.parse".}

327
src/karax.nim Normal file
View file

@ -0,0 +1,327 @@
## Karax -- Single page applications for Nim.
import dom, vdom, jstrutils, components, jdict
export dom.Event
proc kout*[T](x: T) {.importc: "console.log", varargs.}
## the preferred way of debugging karax applications.
proc hasProp(e: Node; prop: cstring): bool {.importcpp: "(#.hasOwnProperty(#))".}
proc rawkey(e: Node): VKey {.importcpp: "#.karaxKey", nodecl.}
proc key*(e: Node): VKey =
if e.hasProp"karaxKey": result = e.rawkey
else: result = -1
proc `key=`*(e: Node; x: VKey) {.importcpp: "#.karaxKey = #", nodecl.}
var
toFocus: Node
toFocusV: VNode
proc setFocus*(n: VNode) =
toFocusV = n
# ----------------- event wrapping ---------------------------------------
template nativeValue(ev): cstring = cast[Element](ev.target).value
template setNativeValue(ev, val) = cast[Element](ev.target).value = val
template keyeventBody() =
n.value = nativeValue(ev)
action(ev, n)
setNativeValue(ev, n.value)
# Do not call redraw() here! That is already done
# by ``karax.addEventHandler``.
proc wrapEvent(d: Node; n: VNode; k: EventKind; action: EventHandler) =
proc stdWrapper(): (proc (ev: Event)) =
let action = action
let n = n
result = proc (ev: Event) =
action(ev, n)
proc enterWrapper(): (proc (ev: Event)) =
let action = action
let n = n
result = proc (ev: Event) =
if ev.keyCode == 13: keyeventBody()
proc laterWrapper(): (proc (ev: Event)) =
let action = action
let n = n
var timer: Timeout
result = proc (ev: Event) =
proc wrapper() = keyeventBody()
if timer != nil: clearTimeout(timer)
timer = setTimeout(wrapper, 400)
case k
of EventKind.onkeyuplater:
d.addEventListener("keyup", laterWrapper())
of EventKind.onkeyupenter:
d.addEventListener("keyup", enterWrapper())
else:
d.addEventListener(toEventName[k], stdWrapper())
# --------------------- DOM diff -----------------------------------------
template detach(n: VNode) = n.dom = nil
template attach(n: Vnode) = n.dom = result
proc vnodeToDom(n: VNode): Node =
if n.kind == VNodeKind.text:
result = document.createTextNode(n.text)
attach n
elif n.kind == VNodeKind.vthunk:
let x = callThunk(vcomponents[n.text], n)
result = vnodeToDom(x)
n.key = result.key
attach n
return result
elif n.kind == VNodeKind.dthunk:
result = callThunk(dcomponents[n.text], n)
n.key = result.key
attach n
return result
else:
result = document.createElement(toTag[n.kind])
attach n
for k in n:
appendChild(result, vnodeToDom(k))
# text is mapped to 'value':
if n.text != nil:
result.value = n.text
if n.id != nil:
result.id = n.id
if n.class != nil:
result.class = n.class
if n.key >= 0:
result.key = n.key
for k, v in attrs(n):
if v != nil:
result.setAttr(k, v)
for e, h in items(n.events):
wrapEvent(result, n, e, h)
if n == toFocusV and toFocus.isNil:
toFocus = result
proc same(n: VNode, e: Node): bool =
if toTag[n.kind] == e.nodename:
result = true
if n.kind != VNodeKind.text:
if e.len != n.len: return false
for i in 0 ..< n.len:
if not same(n[i], e[i]): return false
var
dorender: proc (): VNode {.closure.}
currentTree: VNode
proc replaceById(id: cstring; newTree: Node) =
let x = document.getElementById(id)
x.parentNode.replaceChild(newTree, x)
#newTree.id = id
proc equalsShallow(a, b: VNode): bool =
if a.kind != b.kind: return false
if a.id != b.id: return false
if a.key != b.key: return false
if a.kind == VNodeKind.text:
if a.text != b.text: return false
elif a.kind == VNodeKind.vthunk or a.kind == VNodeKind.dthunk:
if a.text != b.text: return false
if a.len != b.len: return false
for i in 0..<a.len:
if not equalsShallow(a[i], b[i]): return false
if not sameAttrs(a, b): return false
if a.class != b.class: return false
# XXX test event listeners here?
return true
proc equalsTree(a, b: VNode): bool =
when false:
# hashing is too fragile now with component support:
if not a.validHash:
a.calcHash()
if not b.validHash:
b.calcHash()
return a.hash == b.hash
else:
result = eq(a, b)
proc updateDirtyElements(parent, current: Node, newNode: VNode) =
if newNode.key >= 0 and isDirty(newNode.key):
unmarkDirty(newNode.key)
let n = vnodeToDom(newNode)
if parent == nil:
replaceById("ROOT", n)
else:
parent.replaceChild(n, current)
elif newNode.kind != VNodeKind.text and newNode.kind != VNodeKind.vthunk and
newNode.kind != VNodeKind.dthunk:
for i in 0..newNode.len-1:
updateDirtyElements(current, current[i], newNode[i])
# leave early if we know there cannot be anything left to do:
#if dirtyCount <= 0: return
proc updateElement(parent, current: Node, newNode, oldNode: VNode) =
newNode.dom = oldNode.dom
if not equalsShallow(newNode, oldNode):
detach(oldNode)
let n = vnodeToDom(newNode)
if parent == nil:
replaceById("ROOT", n)
else:
parent.replaceChild(n, current)
elif newNode.kind != VNodeKind.text:
let newLength = newNode.len
var oldLength = oldNode.len
assert oldNode.kind == newNode.kind
when defined(simpleDiff):
for i in 0..min(newLength, oldLength)-1:
updateElement(current, current[i], newNode[i], oldNode[i])
if newLength > oldLength:
for i in oldLength..newLength-1:
current.appendChild(vnodeToDom(newNode[i]))
elif oldLength > newLength:
for i in countdown(oldLength-1, newLength):
detach(oldNode[i])
current.removeChild(current.lastChild)
else:
var left = 0
var rightNew = newLength - 1
var rightOld = oldLength - 1
while left <= min(rightNew, rightOld):
var minRight = min(rightOld, rightNew)
while left <= minRight and
equalsTree(newNode[left], oldNode[left]):
newNode[left] = oldNode[left]
inc left
while rightOld > left and rightNew > left and
equalsTree(newNode[rightNew], oldNode[rightOld]):
newNode[rightNew] = oldNode[rightOld]
dec rightOld
dec rightNew
minRight = min(rightOld, rightNew)
if left <= minRight:
updateElement(current, oldNode[left].dom, newNode[left], oldNode[left])
inc left
if left < minRight:
updateElement(current, oldNode[rightOld].dom, newNode[rightNew], oldNode[rightOld])
dec rightNew
dec rightOld
var posNextElem = rightOld + 1
var isPushBack = posNextElem == oldLength
while left <= rightNew:
if isPushBack:
current.appendChild(vnodeToDom(newNode[left]))
else:
current.insertBefore(vnodeToDom(newNode[left]), oldNode[posNextElem].dom)
inc left
for i in left..rightOld:
current.removeChild(oldNode[i].dom)
detach(oldNode[i])
when false:
var drawTimeout: Timeout
proc dodraw() =
if dorender.isNil: return
let newtree = dorender()
newtree.id = "ROOT"
toFocus = nil
if currentTree == nil:
currentTree = newtree
let asdom = vnodeToDom currentTree
replaceById("ROOT", asdom)
else:
let olddom = document.getElementById("ROOT")
updateElement(nil, olddom, newtree, currentTree)
#assert same(newtree, document.getElementById("ROOT"))
if someDirty:
updateDirtyElements(nil, olddom, newtree)
someDirty = false
currentTree = newtree
# now that it's part of the DOM, give it the focus:
if toFocus != nil:
toFocus.focus()
proc reqFrame(callback: proc()) {.importc: "window.requestAnimationFrame".}
proc redraw*() =
# we buffer redraw requests:
when false:
if drawTimeout != nil:
clearTimeout(drawTimeout)
drawTimeout = setTimeout(dodraw, 30)
elif true:
reqFrame(dodraw)
else:
dodraw()
proc redrawForce*() = dodraw()
proc init(ev: Event) =
reqFrame(dodraw)
proc setRenderer*(renderer: proc (): VNode) =
dorender = renderer
window.onload = init
proc setRendererOnly*(renderer: proc (): VNode) =
dorender = renderer
proc setOnloadOnly*() =
window.onload = init
proc addEventHandler*(n: VNode; k: EventKind; action: EventHandler) =
## Implements the foundation of Karax's event management.
## Karax DSL transforms ``tag(onEvent = handler)`` to
## ``tempNode.addEventHandler(tagNode, EventKind.onEvent, wrapper)``
## where ``wrapper`` calls the passed ``action`` and then triggers
## a ``redraw``.
proc wrapper(ev: Event; n: VNode) =
action(ev, n)
redraw()
addEventListener(n, k, wrapper)
proc setOnHashChange*(action: proc (hashPart: cstring)) =
var onhashChange {.importc: "window.onhashchange".}: proc()
var hashPart {.importc: "window.location.hash".}: cstring
proc wrapper() =
action(hashPart)
redraw()
onhashchange = wrapper
{.push stackTrace:off.}
proc setupErrorHandler*() =
## Installs an error handler that transforms native JS unhandled
## exceptions into Nim based stack traces. If `useAlert` is false,
## the error message is put into the console, otherwise `alert`
## is called.
proc stackTraceAsCstring(): cstring = cstring(getStackTrace())
var onerror {.importc: "window.onerror".} =
proc (msg, url: cstring, line, col: int, error: cstring): bool =
var x = cstring"Error: " & msg & "\n" & stackTraceAsCstring()
kout(x)
return true # suppressErrorAlert
{.pop.}
proc prepend(parent, kid: Element) =
parent.insertBefore(kid, parent.firstChild)
proc loadScript*(jsfilename: cstring) =
let body = getElementById("body")
let s = document.createElement("script")
s.setAttr "type", "text/javascript"
s.setAttr "src", jsfilename
body.prepend(s)
redraw()

View file

@ -1,46 +1,25 @@
import macros, vdom, compact, kbase
import macros, karax, vdom, components
from strutils import startsWith, toLowerAscii
when defined(js):
import karax
const
StmtContext = ["kout", "inc", "echo", "dec", "!"]
SpecialAttrs = ["id", "class", "value", "index", "style"]
SpecialAttrs = ["id", "class", "value", "key", "track"]
proc getName(n: NimNode): string =
case n.kind
of nnkIdent, nnkSym:
result = $n
of nnkIdent:
result = $n.ident
of nnkAccQuoted:
result = ""
for i in 0..<n.len:
result.add getName(n[i])
of nnkStrLit..nnkTripleStrLit:
result = n.strVal
of nnkInfix:
# allow 'foo-bar' syntax:
if n.len == 3 and $n[0] == "-":
result = getName(n[1]) & "-" & getName(n[2])
else:
expectKind(n, nnkIdent)
of nnkDotExpr:
result = getName(n[0]) & "." & getName(n[1])
of nnkOpenSymChoice, nnkClosedSymChoice:
result = getName(n[0])
else:
#echo repr n
expectKind(n, nnkIdent)
proc toKstring(n: NimNode): NimNode =
if n.kind == nnkStrLit:
result = newCall(bindSym"kstring", n)
else:
result = copyNimNode(n)
for child in n:
result.add toKstring(child)
proc newDotAsgn(tmp: NimNode, key: string, x: NimNode): NimNode =
result = newTree(nnkAsgn, newDotExpr(tmp, newIdentNode key), x)
@ -53,11 +32,6 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
# (except for the last child of the macros we consider here),
# lets, consts, types can be considered as expressions
# case is complex, calls are assumed to produce a value.
when defined(js):
template evHandler(): untyped = bindSym"addEventHandler"
else:
template evHandler(): untyped = ident"addEventHandler"
case n.kind
of nnkLiterals, nnkIdent, nnkSym, nnkDotExpr, nnkBracketExpr:
if tmpContext != nil:
@ -73,34 +47,15 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
assert n.len == result.len
if L > 0:
result[L-1] = tcall2(result[L-1], tmpContext)
of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkTryStmt,
nnkFinally:
of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkCaseStmt,
nnkTryStmt, nnkFinally:
# recurse for every child:
result = copyNimNode(n)
for x in n:
result.add tcall2(x, tmpContext)
of nnkCaseStmt:
# recurse for children, but don't add call for case ident
result = copyNimNode(n)
result.add n[0]
for i in 1 ..< n.len:
result.add tcall2(n[i], tmpContext)
of nnkProcDef:
let name = getName n[0]
if name.startsWith"on":
# turn it into an anon proc:
let anon = copyNimTree(n)
anon[0] = newEmptyNode()
if tmpContext == nil:
error "no VNode to attach the event handler to"
else:
result = newCall(evHandler(), tmpContext,
newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi"))
else:
result = n
of nnkVarSection, nnkLetSection, nnkConstSection:
result = n
of nnkCallKinds - {nnkInfix}:
of nnkCallKinds:
let op = getName(n[0])
let ck = isComponent(op)
if ck != ComponentKind.None:
@ -120,24 +75,16 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
if x.kind == nnkExprEqExpr:
let key = getName x[0]
if key.startsWith("on"):
result.add newCall(evHandler(),
tmp, newDotExpr(bindSym"EventKind", x[0]), x[1], ident("kxi"))
elif eqIdent(key, "style") and x[1].kind == nnkTableConstr:
result.add newDotAsgn(tmp, key, newCall("style", toKstring x[1]))
result.add newCall(bindSym"addEventHandler",
tmp, newDotExpr(bindSym"EventKind", x[0]), x[1])
elif key in SpecialAttrs:
result.add newDotAsgn(tmp, key, x[1])
if key == "value":
result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
elif eqIdent(key, "setFocus"):
result.add newCall(key, tmp, x[1], ident"kxi")
elif eqIdent(key, "events"):
result.add newCall(bindSym"mergeEvents", tmp, x[1])
else:
result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
elif ck != ComponentKind.Tag:
call.add x
elif eqIdent(x, "setFocus"):
result.add newCall(x, tmp, bindSym"true", ident"kxi")
result.add newCall(x, tmp)
else:
result.add tcall2(x, tmp)
if tmpContext == nil:
@ -145,28 +92,7 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
else:
result.add newCall(bindSym"add", tmpContext, tmp)
elif tmpContext != nil and op notin StmtContext:
var hasEventHandlers = false
for i in 1..<n.len:
let it = n[i]
if it.kind in {nnkProcDef, nnkStmtList}:
hasEventHandlers = true
break
if not hasEventHandlers:
result = newCall(bindSym"add", tmpContext, n)
else:
let tmp = genSym(nskLet, "tmp")
var slicedCall = newCall(n[0])
let ex = newTree(nnkStmtListExpr)
ex.add newEmptyNode() # will become the let statement
for i in 1..<n.len:
let it = n[i]
if it.kind in {nnkProcDef, nnkStmtList}:
ex.add tcall2(it, tmp)
else:
slicedCall.add it
ex[0] = newLetStmt(tmp, slicedCall)
ex.add tmp
result = newCall(bindSym"add", tmpContext, ex)
elif op == "!" and n.len == 2:
result = n[1]
else:
@ -175,22 +101,20 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
result = n
macro buildHtml*(tag, children: untyped): VNode =
let kids = newProc(procType=nnkDo, body=children)
expectKind kids, nnkDo
expectKind children, nnkDo
var call: NimNode
if tag.kind in nnkCallKinds:
call = tag
else:
call = newCall(tag)
call.add body(kids)
call.add body(children)
result = tcall2(call, nil)
when defined(debugKaraxDsl):
echo repr result
macro buildHtml*(children: untyped): VNode =
let kids = newProc(procType=nnkDo, body=children)
expectKind kids, nnkDo
result = tcall2(body(kids), nil)
expectKind children, nnkDo
result = tcall2(body(children), nil)
when defined(debugKaraxDsl):
echo repr result

View file

@ -1,3 +1,3 @@
## Include file that contains the common imports for the Karax framework.
import kbase, karax, karaxdsl, vdom, compact, jstrutils
import karax, karaxdsl, vdom, components

View file

@ -2,9 +2,9 @@
## This module contains wrappers for the HTML 5 local storage.
proc getItem*(key: cstring): cstring {.importc: "localStorage.getItem".}
proc setItem*(key, value: cstring) {.importc: "localStorage.setItem".}
proc getItem*(key: cstring): cstring {.importc: "localStorage.getItem"}
proc setItem*(key, value: cstring) {.importc: "localStorage.setItem"}
proc hasItem*(key: cstring): bool {.importcpp: "(localStorage.getItem(#) !== null)".}
proc clear*() {.importc: "localStorage.clear".}
proc clear*() {.importc: "localStorage.clear"}
proc removeItem*(key: cstring) {.importc: "localStorage.removeItem".}

257
src/vdom.nim Normal file
View file

@ -0,0 +1,257 @@
## Virtual DOM implementation for Karax.
from dom import Event, Node
import shash, macros
from strutils import toUpperAscii
type
VNodeKind* {.pure.} = enum
text = "#text", int = "#int", bool = "#bool",
vthunk = "#vthunk", dthunk = "#dthunk",
html, head, title, base, link, meta, style,
script, noscript,
body, section, nav, article, aside,
h1, h2, h3, h4, h5, h6,
header, footer, address, main
p, hr, pre, blockquote, ol, ul, li,
dl, dt, dd,
figure, figcaption,
tdiv = "div",
a, em, strong, small,
strikethrough = "s", cite, quote,
dfn, abbr, data, time, code, `var` = "var", samp,
kdb, sub, sup, italic = "i", bold = "b", underlined = "u",
mark, ruby, rt, rp, bdi, dbo, span, br, wbr,
ins, del, img, iframe, embed, `object` = "object",
param, video, audio, source, track, canvas, map,
area, svg, math,
table, caption, colgroup, col, tbody, thead,
tfoot, tr, td, th,
form, fieldset, legend, label, input, button,
select, datalist, optgroup, option, textarea,
keygen, output, progress, meter,
details, summary, command, menu
type
EventKind* {.pure.} = enum ## The events supported by the virtual DOM.
onclick, ## An element is clicked.
ondblclick, ## An element is double clicked.
onkeyup, ## A key was released.
onkeydown, ## A key is pressed.
onkeypressed, # A key was pressed.
onblur, ## An element lost the focus.
onchange, ## The selected value of an element was changed.
onscroll, ## The user scrolled within an element.
onmousedown, ## A pointing device button (usually a mouse) is pressed
## on an element.
onmouseenter, ## A pointing device is moved onto the element that
## has the listener attached.
onmouseleave, ## A pointing device is moved off the element that
## has the listener attached.
onmousemove, ## A pointing device is moved over an element.
onmouseout, ## A pointing device is moved off the element that
## has the listener attached or off one of its children.
onmouseover, ## A pointing device is moved onto the element that has
## the listener attached or onto one of its children.
onmouseup, ## A pointing device button is released over an element.
ondrag, ## An element or text selection is being dragged (every 350ms).
ondragend, ## A drag operation is being ended (by releasing a mouse button
## or hitting the escape key).
ondragenter, ## A dragged element or text selection enters a valid drop target.
ondragleave, ## A dragged element or text selection leaves a valid drop target.
ondragover, ## An element or text selection is being dragged over a valid
## drop target (every 350ms).
ondragstart, ## The user starts dragging an element or text selection.
ondrop, ## An element is dropped on a valid drop target.
onkeyupenter, ## vdom extension: an input field received the ENTER key press
onkeyuplater ## vdom extension: a key was pressed and some time
## passed (useful for on-the-fly text completions)
macro buildLookupTables(): untyped =
var a = newTree(nnkBracket)
for i in low(VNodeKind)..high(VNodeKind):
let x = $i
let y = if x[0] == '#': x else: toUpperAscii(x)
a.add(newCall("cstring", newLit(y)))
var e = newTree(nnkBracket)
for i in low(EventKind)..high(EventKind):
e.add(newCall("cstring", newLit(substr($i, 2))))
template tmpl(a, e) {.dirty.} =
const
toTag*: array[VNodeKind, cstring] = a
toEventName*: array[EventKind, cstring] = e
result = getAst tmpl(a, e)
buildLookupTables()
type
EventHandler* = proc (ev: Event; target: VNode) {.closure.}
VKey* = int
VNode* = ref object
kind*: VNodeKind
key*: VKey
id*, class*, text*: cstring
track*: int
kids: seq[VNode]
# even index: key, odd index: value; done this way for memory efficiency:
attrs: seq[cstring]
events*: seq[(EventKind, EventHandler)]
when false:
hash*: Hash
validHash*: bool
dom*: Node ## the attached real DOM node. Can be 'nil' if the virtual node
## is not part of the virtual DOM anymore.
proc value*(n: VNode): cstring = n.text
proc `value=`*(n: VNode; v: cstring) = n.text = v
proc intValue*(n: VNode): int = n.key
proc vn*(i: int): VNode = VNode(kind: VNodeKind.int, key: i)
proc vn*(b: bool): VNode = VNode(kind: VNodeKind.int, key: ord(b))
proc vn*(x: cstring): VNode = VNode(kind: VNodeKind.text, key: -1, text: x)
template callThunk*(fn: typed; n: VNode): untyped =
## for internal usage only.
fn(n.kids)
proc vthunk*(name: cstring; args: varargs[VNode, vn]): VNode =
VNode(kind: VNodeKind.vthunk, text: name, key: -1, kids: @args)
proc dthunk*(name: cstring; args: varargs[VNode, vn]): VNode =
VNode(kind: VNodeKind.dthunk, text: name, key: -1, kids: @args)
proc eq*(a, b: VNode): bool =
if a.kind != b.kind: return false
if a.id != b.id: return false
if a.class != b.class: return false
if a.key != b.key: return false
if a.kind != VNodeKind.text:
if a.kids.len != b.kids.len: return false
for i in 0..<a.kids.len:
if not eq(a.kids[i], b.kids[i]): return false
if a.text != b.text: return false
if a.attrs.len != b.attrs.len: return false
for i in 0..<a.attrs.len:
if a.attrs[i] != b.attrs[i]: return false
result = true
proc setAttr*(n: VNode; key: cstring; val: cstring = "") =
if n.attrs.isNil:
when true:
let x = @[key, val]
shallowCopy n.attrs, x
else:
n.attrs = @[key, val]
else:
for i in countup(0, n.attrs.len-2, 2):
if n.attrs[i] == key:
n.attrs[i+1] = val
return
n.attrs.add key
n.attrs.add val
proc getAttr*(n: VNode; key: cstring): cstring =
for i in countup(0, n.attrs.len-2, 2):
if n.attrs[i] == key: return n.attrs[i+1]
proc len*(x: VNode): int = x.kids.len
proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx]
proc `[]=`*(x: VNode; idx: int; y: VNode) = x.kids[idx] = y
proc add*(parent, kid: VNode) = parent.kids.add kid
proc newVNode*(kind: VNodeKind): VNode = VNode(kind: kind, key: -1)
proc tree*(kind: VNodeKind; kids: varargs[VNode]): VNode =
result = newVNode(kind)
for k in kids: result.add k
proc tree*(kind: VNodeKind; attrs: openarray[(cstring, cstring)];
kids: varargs[VNode]): VNode =
result = tree(kind, kids)
for a in attrs: result.setAttr(a[0], a[1])
proc text*(s: string): VNode = VNode(kind: VNodeKind.text, text: cstring(s), key: -1)
proc text*(s: cstring): VNode = VNode(kind: VNodeKind.text, text: s, key: -1)
iterator items*(n: VNode): VNode =
for i in 0..<n.kids.len: yield n.kids[i]
iterator attrs*(n: VNode): (cstring, cstring) =
for i in countup(0, n.attrs.len-2, 2):
yield (n.attrs[i], n.attrs[i+1])
proc sameAttrs*(a, b: VNode): bool =
if a.attrs.len == b.attrs.len:
result = true
for i in 0 ..< a.attrs.len:
if a.attrs[i] != b.attrs[i]: return false
proc addEventListener*(n: VNode; event: EventKind; handler: EventHandler) =
n.events.add((event, handler))
template toStringAttr(field) =
if n.field != nil:
result.add " " & astToStr(field) & " = " & $n.field
proc toString*(n: VNode; result: var string; indent: int) =
for i in 1..indent: result.add ' '
if result.len > 0: result.add '\L'
result.add "<" & $n.kind
toStringAttr(id)
toStringAttr(class)
for k, v in attrs(n):
result.add " " & $k & " = " & $v
result.add ">\L"
if n.kind == VNodeKind.text:
result.add n.text
else:
if n.text != nil:
result.add " value = "
result.add n.text
for child in items(n):
toString(child, result, indent+2)
for i in 1..indent: result.add ' '
result.add "\L</" & $n.kind & ">"
when false:
proc calcHash*(n: VNode) =
if n.validHash: return
n.validHash = true
var h: Hash = ord n.kind
if n.id != nil:
h &= "id"
h &= n.id
if n.class != nil:
h &= "class"
h &= n.class
if n.key >= 0:
h &= "k"
h &= n.key
for k, v in attrs(n):
h &= " "
h &= k
h &= "="
h &= v
if n.kind == VNodeKind.text or n.text != nil:
h &= "t"
h &= n.text
else:
for child in items(n):
calcHash(child)
h &= child.hash
n.hash = h
proc `$`*(n: VNode): cstring =
var res = ""
toString(n, res, 0)
result = cstring(res)

View file

@ -1,133 +0,0 @@
import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils
type TextInput* = ref object of VComponent
value, guid: cstring
isActive: bool
onchange: proc (value: cstring)
proc render(x: VComponent): VNode =
let self = TextInput(x)
let style = style(
(StyleAttr.position, cstring"relative"),
(StyleAttr.paddingLeft, cstring"10px"),
(StyleAttr.paddingRight, cstring"5px"),
(StyleAttr.height, cstring"30px"),
(StyleAttr.lineHeight, cstring"30px"),
(StyleAttr.border, cstring"solid 1px " & (if self.isActive: cstring"red" else: cstring"black")),
(StyleAttr.fontSize, cstring"12px"),
(StyleAttr.fontWeight, cstring"600")
).merge(self.style)
let inputStyle = style.merge(style(
(StyleAttr.color, cstring"inherit"),
(StyleAttr.fontSize, cstring"inherit"),
(StyleAttr.fontWeight, cstring"inherit"),
(StyleAttr.fontFamily, cstring"inherit"),
(StyleAttr.position, cstring"absolute"),
(StyleAttr.top, cstring"0"),
(StyleAttr.left, cstring"0"),
(StyleAttr.height, cstring"100%"),
(StyleAttr.width, cstring"100%"),
(StyleAttr.border, cstring"none"),
(StyleAttr.backgroundColor, cstring"transparent"),
))
proc flip(ev: Event; n: VNode) =
self.isActive = not self.isActive
echo "flip! ", self.isActive, " id: ", self.debugId, " version ", self.version
markDirty(self)
proc onchanged(ev: Event; n: VNode) =
if self.onchange != nil:
self.onchange n.value
self.value = n.value
result = buildHtml(tdiv(style=style)):
input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip, onkeyup=onchanged)
proc changed(current, next: VComponent): bool =
let current = TextInput(current)
let next = TextInput(next)
if current.guid != next.guid:
result = true
else:
result = defaultChangedImpl(current, next)
proc update(current, next: VComponent) =
let current = TextInput(current)
let next = TextInput(next)
current.value = next.value
current.guid = next.guid
#next.isActive = current.isActive
proc newTextInput*(style: VStyle = VStyle(); guid: cstring; value: cstring = cstring"",
onchange: proc(v: cstring) = nil): TextInput =
result = newComponent(TextInput, render, changed=changed, updated=update)
result.style = style
result.value = value
result.onchange = onchange
result.guid = guid
when false:
type
Combined = ref object of VComponent
a, b: TextInput
proc renderComb(self: VComponent): VNode =
let self = Combined(self)
proc bu(ev: Event; n: VNode) =
self.a.value = ""
self.b.value = ""
markDirty(self.a)
markDirty(self.b)
result = buildHtml(tdiv(style=self.style)):
self.a
self.b
button(onclick=bu):
text "reset"
proc changed(self: VComponent): bool =
let self = Combined(self)
result = self.a.changedImpl(self.a) or self.b.changedImpl(self.b)
proc newCombined*(style: VStyle = VStyle()): Combined =
result = newComponent(Combined, renderComb, changed=changed)
result.a = newTextInput(style, "AAA")
result.b = newTextInput(style, "BBB")
var
persons: seq[cstring] = @[cstring"Karax", "Abathur", "Fenix"]
selected = -1
errmsg = cstring""
proc renderPerson(text: cstring, index: int): VNode =
proc select(ev: Event, n: VNode) =
selected = index
errmsg = ""
result = buildHtml():
tdiv:
text text
button(onClick = select)
proc createDom(): VNode =
result = buildHtml(tdiv):
tdiv:
for index, text in persons.pairs:
renderPerson(text, index)
tdiv:
newTextInput(VStyle(), &selected, if selected >= 0: persons[selected] else: "", proc (v: cstring) =
if v.len > 0:
if selected >= 0: persons[selected] = v
errmsg = ""
else:
errmsg = "name must not be empty"
)
tdiv:
text errmsg
setRenderer createDom

View file

@ -1,18 +0,0 @@
include ../karax/prelude
block:
type
TestEnum {.pure.} = enum
ValA, ValB
var e = TestEnum.ValA
proc renderDom(): VNode {.used.} =
result = buildHtml():
tdiv():
case e
of TestEnum.ValA:
tdiv: text "A"
of TestEnum.ValB:
tdiv: text "B"

View file

@ -1,128 +0,0 @@
import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils
type TextInput* = ref object of VComponent
value: cstring
isActive: bool
proc render(x: VComponent): VNode =
let self = TextInput(x)
let style = style(
(StyleAttr.position, cstring"relative"),
(StyleAttr.paddingLeft, cstring"10px"),
(StyleAttr.paddingRight, cstring"5px"),
(StyleAttr.height, cstring"30px"),
(StyleAttr.lineHeight, cstring"30px"),
(StyleAttr.border, cstring"solid 1px " & (if self.isActive: cstring"red" else: cstring"black")),
(StyleAttr.fontSize, cstring"12px"),
(StyleAttr.fontWeight, cstring"600")
).merge(self.style)
let inputStyle = style.merge(style(
(StyleAttr.color, cstring"inherit"),
(StyleAttr.fontSize, cstring"inherit"),
(StyleAttr.fontWeight, cstring"inherit"),
(StyleAttr.fontFamily, cstring"inherit"),
(StyleAttr.position, cstring"absolute"),
(StyleAttr.top, cstring"0"),
(StyleAttr.left, cstring"0"),
(StyleAttr.height, cstring"100%"),
(StyleAttr.width, cstring"100%"),
(StyleAttr.border, cstring"none"),
(StyleAttr.backgroundColor, cstring"transparent"),
))
proc flip(ev: Event; n: VNode) =
self.isActive = not self.isActive
echo "flip! ", self.isActive, " id: ", self.debugId, " version ", self.version
markDirty(self)
result = buildHtml(tdiv(style=style)):
input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip,
events=self.events)
proc update(current, next: VComponent) =
let current = TextInput(current)
let next = TextInput(next)
current.value = next.value
current.key = next.key
proc changed(current, next: VComponent): bool = true
proc newTextInput*(style: VStyle = VStyle(); key: cstring;
value = cstring""): TextInput =
result = newComponent(TextInput, render, changed=changed, updated=update)
result.style = style
result.value = value
#result.key = key
when false:
type
Combined = ref object of VComponent
a, b: TextInput
proc renderComb(self: VComponent): VNode =
let self = Combined(self)
proc bu(ev: Event; n: VNode) =
self.a.value = ""
self.b.value = ""
markDirty(self.a)
markDirty(self.b)
result = buildHtml(tdiv(style=self.style)):
self.a
self.b
button(onclick=bu):
text "reset"
proc changed(self: VComponent): bool =
let self = Combined(self)
result = self.a.changedImpl(self.a) or self.b.changedImpl(self.b)
proc newCombined*(style: VStyle = VStyle()): Combined =
result = newComponent(Combined, renderComb, changed=changed)
result.a = newTextInput(style, "AAA")
result.b = newTextInput(style, "BBB")
var
persons: seq[cstring] = @[cstring"Karax", "Abathur", "Fenix"]
errmsg = cstring""
proc renderPerson(text: cstring, index: int): VNode =
result = buildHtml(tdiv):
newTextInput(VStyle(), &index, text):
proc onkeyuplater(ev: Event; n: VNode) =
let v = n.value
if v.len > 0:
persons[index] = v
errmsg = ""
else:
errmsg = "name must not be empty"
button:
proc onclick(ev: Event; n: VNode) =
persons.delete(index)
errmsg = ""
echo persons
text "(x)"
proc createDom(): VNode =
result = buildHtml(tdiv):
tdiv:
for index, text in persons.pairs:
renderPerson(text, index)
tdiv:
newTextInput(VStyle(), &persons.len, ""):
proc onkeyupenter(ev: Event; n: VNode) =
let v = n.value
if v.len > 0:
persons.add v
errmsg = ""
else:
errmsg = "name must not be empty"
tdiv:
text errmsg
setRenderer createDom

View file

@ -3,13 +3,14 @@
<html>
<head>
<title>Diff dom tests</title>
<link rel="stylesheet" href="style.css">
</head>
<body id="body">
<div id="ROOT" />
<script type="text/javascript" src="diffDomTests.js"></script>
<script type="text/javascript" src="nimcache/diffDomTests.js"></script>
<script type="text/javascript">
onload(window.location.pathname.split('/')[1])
</script>

View file

@ -1,76 +1,129 @@
import kdom, vdom, times, karax, karaxdsl, jdict, jstrutils, parseutils, sequtils
import dom, vdom, times, karax, karaxdsl, jdict, jstrutils, parseutils, sequtils
var
entries: seq[cstring]
results: seq[cstring]
timeout: Timeout
proc reset() =
results.add cstring"reset started"
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5") ]
redrawSync()
redraw()
results.add cstring"reset finished"
proc checkOrder(order : seq[int]): bool =
var ul = getElementById("ul")
if ul == nil or len(ul.children) != len(order):
kout ul, len(order)
return false
var pos = 0
for child in ul.children:
if child.id != $order[pos]:
kout pos
return false
inc pos
return true
proc check(name: cstring; order: seq[int]) =
let result = checkOrder(order)
results.add name & (if result: cstring" - OK" else: cstring" -FAIL")
proc check1() =
let result = checkOrder(@[0, 1, 2, 3, 4, 7, 5])
if result:
results.add cstring"test1 - OK"
else:
results.add cstring"test1 - FAIL"
# result: 0 1 2 3 4 7 5
proc test1() =
results.add cstring"test1 started"
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5") ]
entries.insert(cstring("7"), 5)
redrawSync()
check("test1", @[0, 1, 2, 3, 4, 7, 5])
redraw()
timeout = setTimeout(check1, 20)
proc check2() =
let result = checkOrder(@[8, 0, 1, 2, 3, 4, 7, 5])
if result:
results.add cstring"test1 - OK"
else:
results.add cstring"test2 - FAIL"
# result: 8 0 1 2 3 4 7 5
proc test2() =
results.add cstring"test2 started"
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5") ]
entries.insert(cstring("7"), 5)
entries.insert(cstring("8"), 0)
redrawSync()
check("test2", @[8, 0, 1, 2, 3, 4, 7, 5])
redraw()
timeout = setTimeout(check2, 20)
proc check3() =
let result = checkOrder(@[2, 3, 4, 1])
if result:
results.add cstring"test3 - OK"
else:
results.add cstring"test3 - FAIL"
# result: 2 3 4 1
proc test3() =
results.add cstring"test3 started"
entries = @[cstring("2"), cstring("3"), cstring("4"), cstring("1") ]
redrawSync()
check("test3", @[2, 3, 4, 1])
redraw()
timeout = setTimeout(check3, 20)
proc check4() =
let result = checkOrder(@[5, 6, 7, 8])
if result:
results.add cstring"test4 - OK"
else:
results.add cstring"test4 - FAIL"
# result: 5 6 7 8
proc test4() =
results.add cstring"test4 started"
entries = @[cstring("5"), cstring("6"), cstring("7"), cstring("8") ]
redrawSync()
check("test4", @[5, 6, 7, 8])
redraw()
timeout = setTimeout(check4, 20)
proc check5() =
let result = checkOrder(@[0, 1, 3, 5, 4, 5])
if result:
results.add cstring"test5 - OK"
else:
results.add cstring"test5 - FAIL"
# result: 0 1 3 5 4 5
proc test5() =
results.add cstring"test5 started"
entries = @[cstring("0"), cstring("1"), cstring("3"), cstring("5"), cstring("4"), cstring("5") ]
redrawSync()
check("test 5", @[0, 1, 3, 5, 4, 5])
redraw()
timeout = setTimeout(check5, 20)
proc check6() =
let result = checkOrder(@[])
if result:
results.add cstring"test6 - OK"
else:
results.add cstring"test6 - FAIL"
# result: empty
proc test6() =
results.add cstring"test6 started"
entries = @[]
redrawSync()
check("test 6", @[])
redraw()
timeout = setTimeout(check6, 20)
proc check7() =
let result = checkOrder(@[2])
if result:
results.add cstring"test7 - OK"
else:
results.add cstring"test7 - FAIL"
redraw()
# result: 2
proc test7() =
results.add cstring"test7 started"
entries = @[cstring("2")]
redrawSync()
check("test 7", @[2])
redraw()
timeout = setTimeout(check7, 20)
proc createEntry(id: int): VNode =
result = buildHtml():
@ -89,19 +142,42 @@ proc createDom(): VNode =
proc onload() =
for i in 0..5: # 0_000:
entries.add(cstring($i))
test1()
reset()
test2()
reset()
test3()
reset()
test4()
reset()
test5()
reset()
test6()
reset()
test7()
setRenderer createDom
var dtReset = 100
var dtTest = 500
var t = dtReset
timeout = setTimeout(test1, t)
t += dtTest
timeout = setTimeout(reset, t)
t += dtReset
timeout = setTimeout(test2, t)
t += dtTest
timeout = setTimeout(reset, t)
t += dtReset
timeout = setTimeout(test3, t)
t += dtTest
timeout = setTimeout(reset, t)
t += dtReset
timeout = setTimeout(test4, t)
t += dtTest
timeout = setTimeout(reset, t)
t += dtReset
timeout = setTimeout(test5, t)
t += dtTest
timeout = setTimeout(reset, t)
t += dtReset
timeout = setTimeout(test6, t)
t += dtTest
timeout = setTimeout(reset, t)
t += dtReset
timeout = setTimeout(test7, t)
onload()
setRenderer createDom

1
tests/diffDomTests.nims Normal file
View file

@ -0,0 +1 @@
--path: "../src"

View file

@ -1,185 +0,0 @@
include "../karax/karax"
import "../karax/karaxdsl"
proc hasDom(n: Vnode) =
if n.kind in {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}:
discard
else:
doAssert n.dom != nil
for i in 0..<n.len: hasDom(n[i])
proc shortRepr(n: VNode): string =
if n == nil:
result = "nil"
elif n.kind == VNodeKind.text:
result = $n.text
else:
result = $n.kind
for i in 0..<n.len:
result &= " " & shortRepr(n[i])
var err = 0
proc doDiff(a, b: VNode; expected: varargs[string]) =
diff(b, a, nil, vnodeToDom(a, kxi), kxi)
var j = 0
for i in 0..<kxi.patchLen:
if kxi.patches[i].k != pkSame:
let n = if kxi.patches[i].k == pkDetach: kxi.patches[i].oldNode else: kxi.patches[i].newNode
let p = $kxi.patches[i].k & " " & shortRepr(n)
if j >= expected.len:
echo "patches differ; expected nothing but got: ", p
inc err
elif p != expected[j]:
echo "patches differ; expected ", expected[i], " but got: ", p
inc err
inc j
#hasDom(kxi.currentTree)
kxi.patchLen = 0
proc testAppend() =
let a = buildHtml(tdiv):
ul:
li: text "A"
li: text "B"
let b = buildHtml(tdiv):
ul:
li: text "A"
li: text "B"
li:
tdiv:
text "C"
doDiff(a, b, "pkAppend li div C")
proc testInsert() =
let a = buildHtml(tdiv):
ul:
li: text "A"
li:
button:
text "C"
let b = buildHtml(tdiv):
ul:
li: text "A"
li: text "B"
li:
button:
text "C"
doDiff(a, b, "pkInsertBefore li B")
proc testInsert2() =
let a = buildHtml(tdiv):
tdiv:
tdiv:
ul:
li: text "A"
li:
button:
text "D"
let b = buildHtml(tdiv):
tdiv:
tdiv:
ul:
li: text "A"
li: text "B"
li: text "C"
li:
button:
text "D"
doDiff(a, b, "pkInsertBefore li B", "pkInsertBefore li C")
proc testDelete() =
let a = buildHtml(tdiv):
ul:
li: text "A"
li: text "B"
li: text "C"
let b = buildHtml(tdiv):
ul:
discard
doDiff(a, b, "pkDetach li A", "pkRemove nil",
"pkDetach li B", "pkRemove nil",
"pkDetach li C", "pkRemove nil")
proc testDeleteMiddle() =
let a = buildHtml(tdiv):
ul:
li:
tdiv: text "A"
li:
tdiv: text "B"
li:
tdiv: text "C"
li:
tdiv: text "D"
li:
tdiv: text "E"
li:
tdiv: text "F"
li:
tdiv: text "G"
li:
button:
tdiv: text "H"
let b = buildHtml(tdiv):
ul:
li:
tdiv: text "A"
li:
tdiv: text "B"
li:
tdiv: text "C"
li:
tdiv: text "D"
li:
tdiv: text "E"
li:
tdiv: text "F"
li:
button:
tdiv: text "H"
doDiff(a, b, "pkDetach li div G", "pkRemove nil")
proc createEntry(id: cstring): VNode =
result = buildHtml():
button(id="" & id):
text id
proc createEntries(entries: seq[cstring]): VNode =
result = buildHtml(tdiv()):
ul(id="ul"):
for e in entries:
createEntry(e)
for r in entries:
tdiv:
text r
proc testWild() =
var entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring"7", cstring("5")]
let a = createEntries(entries)
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
let b = createEntries(entries)
doDiff(a, b, "pkDetach button 7", "pkRemove nil", "pkDetach div 7", "pkRemove nil")
proc testWildInsert() =
var entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
let a = createEntries(entries)
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring"7", cstring("5")]
let b = createEntries(entries)
doDiff(a, b, "pkInsertBefore button 7", "pkInsertBefore div 7")
kxi = KaraxInstance(rootId: cstring"ROOT", renderer: proc (data: RouterData): VNode = discard,
byId: newJDict[cstring, VNode]())
testAppend()
testInsert()
testInsert2()
testDelete()
testWild()
testWildInsert()
testDeleteMiddle()
if err == 0:
echo "Success"
else:
quit 1

View file

@ -1,33 +0,0 @@
#dragDiv {
background-color: #0d1224;
width: 500px;
height: 100px;
position: absolute;
top: 500px;
left: 200px;
text-align: center;
cursor: move;
color: rgb(60, 201, 163);
}
#results {
font-family: monospace;
font-size: 10pt;
width: 100%;
height: 400px;
overflow: auto;
background-color: #000000;
color: forestgreen;
}
#test10input1, #test10input2, #test11h3, #test12input {
visibility: hidden;
}
#test11h3 {
color: gold;
}
body {
background-color: dimgrey;
}

View file

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>DOM Event Tests</title>
<link href="domEventTests.css" rel="stylesheet" />
<meta charset="UTF-8" />
<script src="jquery-3.2.1.js" type="text/javascript"></script>
</head>
<body id="body">
<div id="ROOT"></div>
<script src="domEventTests.js" type="text/javascript"></script>
</body>
</html>

View file

@ -1,624 +0,0 @@
import times, jstrutils, strutils, kdom, karax, karaxdsl, vdom, tables
var
results: seq[cstring]
dragEventsCompleted = initTable[DragEventTypes, bool]()
testRunning: array[13, bool]
testCompleted: array[13, bool]
test2ImageDrop = false
dblClickCount = 0
test6ClickCount = 0
test8Failure = false
test10inputFocusCount = 0
test12Failure = false
proc getLogText(text: string): cstring =
result = "[$#]: $#" % [format(getLocalTime(getTime()), "HH:mm:ss"), $text]
proc log(text: string, clear: bool = false) =
if clear:
results = @[]
results.add getLogText(text)
echo text
redrawSync()
# Scroll to top after adding text using JQuery
discard window.setTimeout("$('#results').animate({ scrollTop: 0 }, 100);", 100)
proc check(cond: bool, msg: string) =
if not cond:
log "FAIL: " & msg
doAssert(cond, msg)
proc check(cond: bool, msg: string, warn: bool): bool =
if not cond:
if warn:
log "WARNING: " & msg
else:
log "FAIL: " & msg
doAssert(cond, msg)
result = cond
proc dragDivDragStart(ev: kdom.Event) =
dragEventsCompleted[DragStart] = true
var dgev = cast[DragEvent](ev)
var dragDiv = document.getElementById("dragDiv")
if dragDiv != nil:
var style = window.getComputedStyle(dgev.target)
var dataX = $(parseInt(style.left) - dgev.clientX)
var dataY = $(parseInt(style.top) - dgev.clientY)
dgev.dataTransfer.setData("text/plain", dataX & "," & dataY)
var img = (ImageElement)document.createElement("img")
img.src ="https://nim-lang.org/assets/img/logo.svg"
dgev.dataTransfer.setDragImage(img, 0, 0)
proc dragDivDragOver(ev: kdom.Event) =
dragEventsCompleted[DragOver] = true
ev.preventDefault()
proc checkAllTests() =
var allTestsPassed = true
for a in testCompleted:
if not a:
allTestsPassed = false
break
if allTestsPassed:
log "All tests completed successfully!"
else:
log "Some tests failed. Check the log."
proc completeTest13() =
testRunning[12] = false
testCompleted[12] = true
log "Test 13 completed successfully."
checkAllTests()
proc test13() =
testRunning[12] = true
log "Test 13 started."
log "This test verifies KeyboardEvent getModifierState functionality."
log "To continue, press the left CTRL key."
proc test12focus1(ev: kdom.Event) =
ev.stopImmediatePropagation()
proc test12focus2(ev: kdom.Event) =
test12Failure = true
check false, "Unexpected listener invocation."
proc test12focus3(ev: kdom.Event) =
test12Failure = true
check false, "Unexpected listener invocation."
proc completeTest12() =
if not test12Failure:
var elmt = document.getElementById("test12input")
elmt.style.visibility = "hidden"
testRunning[11] = false
testCompleted[11] = true
log "Test 12 completed successfully."
test13()
proc test12() =
testRunning[11] = true
log "Test 12 started."
log "This test verifies stopImmediatePropagation functionality."
var elmt = document.getElementById("test12input")
elmt.style.visibility = "visible"
elmt.addEventListener($Focus, test12focus1)
elmt.addEventListener($Focus, test12focus2)
elmt.addEventListener($Focus, test12focus3)
elmt.focus()
discard setTimeout(completeTest12, 500)
proc completeTest11() =
document.querySelector("h3").style.visibility = "hidden"
testRunning[10] = false
testCompleted[10] = true
log "Test 11 completed."
test12()
proc test11() =
testRunning[10] = true
log "Test 11 started."
log "This test checks querySelector and querySelectorAll."
document.getElementById("test11h3").style.visibility = "visible"
var elmt = document.querySelector("h3")
check elmt != nil, "querySelector returned nil."
log "text: " & $elmt.innerHTML
check $elmt.innerHTML == "test 11", "Unexpected h3 value."
var elmts = document.querySelectorAll("h3")
log "elmts len: " & $elmts.len
check elmts.len == 1, "Unexpected elmts len."
log "text: " & $elmts[0].innerHTML
check $elmts[0].innerHTML == "test 11", "Unexpected h3 value."
discard setTimeout(completeTest11, 500)
proc completeTest10() =
testRunning[9] = false
testCompleted[9] = true
var test10input1 = document.getElementById("test10input1")
var test10input2 = document.getElementById("test10input2")
test10input1.style.visibility = "hidden"
test10input2.style.visibility = "hidden"
log "Test 10 completed."
test11()
proc test10inputOnfocus(ev: kdom.Event) =
if testRunning[9]:
inc test10inputFocusCount
if test10inputFocusCount > 3:
discard setTimeout(completeTest10, 500)
proc test10() =
log "Test 10 started."
log "This test checks element.focus()."
testRunning[9] = true
var test10input1 = document.getElementById("test10input1")
var test10input2 = document.getElementById("test10input2")
test10input1.style.visibility = "visible"
test10input2.style.visibility = "visible"
test10input1.addEventListener($DomEvent.Focus, test10inputOnfocus)
test10input2.addEventListener($DomEvent.Focus, test10inputOnfocus)
test10input1.focus()
test10input2.focus()
test10input1.focus()
test10input2.focus()
proc completeTest9() =
var test9Value = $document.getElementById("test9value").value
doAssert(test9Value == "[test9]", "Unexpected test9 value.")
testRunning[8] = false
testCompleted[8] = true
test10()
proc test9() =
log "Test 9 started."
log "This tests the setTimeout(w: Window, code: cstring, pause: int) proc."
testRunning[8] = true
discard window.setTimeout("document.getElementById('test9value').value = '[test9]';", 500)
discard window.setTimeout(completeTest9, 1000)
proc completeTest8() =
if not test8Failure:
log "Test 8 completed successfully."
testRunning[7] = false
testCompleted[7] = true
test9()
else:
log "Test 8 failed."
proc failTest8() =
test8Failure = true
check(false, "ClearTimeout failed.")
proc test8() =
log "Test 8 started."
log "This test checks that clearTimeout() works correctly."
testRunning[7] = true
var timeout = setTimeout(failTest8, 1000)
clearTimeout(timeout)
discard setTimeout(completeTest8, 1200)
proc test7() =
log "Test 7 started."
log "This test ensures that mouse events correctly pass button and key modifier information."
log "To continue, right-click while holding SHIFT."
testRunning[6] = true
# A mouseMove handler is needed (even if empty) so mouseDown will have a value for movementX and movementY
proc mouseMove(ev: kdom.Event) =
discard
proc mouseDown(ev: kdom.Event) =
var me = (MouseEvent)ev
if testRunning[5]:
log "[ME] button: " & $me.button
log "[ME] buttons: " & $me.buttons
log "[ME] altKey: " & $me.altKey
log "[ME] ctrlKey: " & $me.ctrlKey
log "[ME] metaKey: " & $me.metaKey
log "[ME] shiftKey: " & $me.shiftKey
log "[ME] clientX: " & $me.clientX
check me.clientX > 0, "Unexpected clientX value."
log "[ME] clientY: " & $me.clientY
check me.clientY > 0, "Unexpected clientY value."
log "[ME] movementX: " & $me.movementX
check me.movementX > 0, "Unexpected movementX value."
log "[ME] movementY: " & $me.movementY
check me.movementY > 0, "Unexpected movementY value."
log "[ME] offsetX: " & $me.offsetX
check me.offsetX > 0, "Unexpected offsetX value."
log "[ME] offsetY: " & $me.offsetY
check me.offsetY > 0, "Unexpected offsetY value."
log "[ME] pageX: " & $me.pageX
check me.pageX > 0, "Unexpected pageX value."
log "[ME] pageY: " & $me.pageY
check me.pageY > 0, "Unexpected pageY value."
log "[ME] relatedTarget == nil: " & $(me.relatedTarget == nil)
log "[ME] screenX: " & $me.screenX
check me.screenX > 0, "Unexpected screenX value."
log "[ME] screenY: " & $me.screenY
check me.screenY > 0, "Unexpected screenY value."
log "[ME] x: " & $me.x
check me.x > 0, "Unexpected x value."
log "[ME] y: " & $me.y
check me.y > 0, "Unexpected y value."
if dblClickCount == 1:
inc test6ClickCount
if test6ClickCount > 1:
testRunning[5] = false
testCompleted[5] = true
log "Test 6 completed successfully."
test7()
elif testRunning[6]:
log "[ME] buttons: " & $me.buttons
var passed = check(me.buttons == (int)SecondaryButton, "Unexpected buttons value.", true)
log "[ME] shiftKey: " & $me.shiftKey
passed = passed and check(me.shiftKey, "Unexpected shiftKey value.", true)
var isShiftPressed = me.getModifierState($KeyboardEventKey.Shift)
log "[ME] getModifierState shift result: " & $isShiftPressed
passed = passed and check(isShiftPressed, "Unexpected getModifierState shift result.", true)
if passed:
log "Test 7 completed successfully."
testRunning[6] = false
testCompleted[6] = true
ev.preventDefault()
if testCompleted[6]:
test8()
proc dblClick(ev: kdom.Event) =
inc dblClickCount
check dblClickCount == 1, "Unexpected double-click event invocation."
log "Try double-clicking again."
proc test6() =
log "Test 6 started."
log "This test validates that addEventListener can be called by passing AddEventListenerOptions, and the options work correctly."
log "To continue, try double-clicking the mouse."
var options = AddEventListenerOptions(once:true)
document.addEventListener($DomEvent.DblClick, dblClick, options)
testRunning[5] = true
proc test5() =
log "Test 5 started."
log "This test validates the newly added properties: window.screen.width, window.scren.height, element.clientWidth, and element.clientHeight."
log "window.screen.width: " & $window.screen.width
check(window.screen.width > 0, "Unexpected value for window.screen.width.")
log "window.screen.height: " & $window.screen.height
check(window.screen.height > 0, "Unexpected value for window.screen.height.")
var elem = document.getElementById("results")
log "results div clientWidth: " & $elem.clientWidth
check(elem.clientWidth > 0, "Unexpected value for clientWidth.")
log "results div clientHeight: " & $elem.clientHeight
check(elem.clientHeight > 0, "Unexpected value for clientHeight.")
testRunning[4] = false
testCompleted[4] = true
log "Test 5 completed successfully."
test6()
proc keyDown(ev: kdom.Event) =
var kbev = ((KeyboardEvent)ev)
if testRunning[2]:
log "[KB] altKey: " & $kbev.altKey
log "[KB] ctrlKey: " & $kbev.ctrlKey
log "[KB] metaKey: " & $kbev.metaKey
log "[KB] shiftKey: " & $kbev.shiftKey
log "[KB] code: " & $kbev.code
log "[KB] isComposing: " & $kbev.isComposing
log "[KB] key: " & $kbev.key
log "[KB] keyCode: " & $kbev.keyCode
log "[KB] location: " & $kbev.location
log "Test 3 completed successfully."
testRunning[2] = false
testCompleted[2] = true
testRunning[3] = true
log "Test 4 started."
log "This test ensures the child events have access to base event settings."
log "[KB] event.bubbles: " & $kbev.bubbles
check kbev.bubbles, "Unexpected event.bubbles value."
log "[KB] event.cancelBubble: " & $kbev.cancelBubble
check kbev.cancelBubble == false, "Unexpected event.cancelBubble value."
log "[KB] event.cancelable: " & $kbev.cancelable
check kbev.cancelable, "Unexpected event.cancelable value."
log "[KB] event.composed: " & $kbev.composed
check kbev.composed, "Unexpected event.composed value."
log "[KB] event.currentTarget.nodeName: " & $kbev.currentTarget.nodeName
check kbev.currentTarget.nodeName == "#document", "Unexpected event.currentTarget.nodeName value."
log "[KB] event.defaultPrevented: " & $kbev.defaultPrevented
check kbev.defaultPrevented == false, "Unexpected event.defaultPrevented value."
log "[KB] event.eventPhase: " & $kbev.eventPhase
check EventPhase(kbev.eventPhase) == BubblingPhase, "Unexpected event.eventPhase value."
log "[KB] event.isTrusted: " & $kbev.isTrusted
check kbev.isTrusted, "Unexpected event.isTrusted value."
log "[UI] kbev.detail: " & $kbev.detail
log "[UI] kbev.view.outerHeight: " & $kbev.view.outerHeight
check kbev.view.outerHeight > 0, "Unexpected view.outerHeight value."
log "[UI] kbev.view.outerWidth: " & $kbev.view.outerWidth
check kbev.view.outerWidth > 0, "Unexpected view.outerWidth value."
testRunning[3] = false
testCompleted[3] = true
log "Test 4 completed."
test5()
elif testRunning[12]:
if kbev.code == "ControlLeft":
log "[KB] altKey: " & $kbev.altKey
log "[KB] ctrlKey: " & $kbev.ctrlKey
log "[KB] metaKey: " & $kbev.metaKey
log "[KB] shiftKey: " & $kbev.shiftKey
log "[KB] code: " & $kbev.code
log "[KB] isComposing: " & $kbev.isComposing
log "[KB] key: " & $kbev.key
log "[KB] keyCode: " & $kbev.keyCode
log "[KB] location: " & $kbev.location
var isControl = kbev.getModifierState($KeyboardEventKey.Control)
log "[KB] getModifierState result: " & $isControl
check isControl, "Unexpected getModifierState return value."
completeTest13()
proc test3() =
log "Test 3 started."
log "This test validates KeyboardEvent handling."
log "Click on the browser to focus, and then press any key."
testRunning[2] = true
proc dragDivDrop(ev: kdom.Event) =
dragEventsCompleted[Drop] = true
var dgev = cast[DragEvent](ev)
var dragDiv = document.getElementById("dragDiv")
var data = $dgev.dataTransfer.getData("text/plain")
var dt = dgev.dataTransfer
if not test2ImageDrop:
var offset = data.split(",")
var newLeft = dgev.clientX + parseInt(offset[0])
var newTop = dgev.clientY + parseInt(offset[1])
if newLeft < 0: newLeft = 0
if newTop < 0: newTop = 0
if newLeft + dragDiv.clientWidth > window.outerWidth: newLeft = window.outerWidth - dragDiv.clientWidth
if newTop + dragDiv.clientHeight > window.outerHeight: newTop = window.outerHeight - dragDiv.clientHeight
dragDiv.style.left = $newLeft & "px"
dragDiv.style.top = $newTop & "px"
if testRunning[0]:
log "[drop] Drop coords: $#, $#" % [$newLeft, $newTop]
if testCompleted[0] and not testCompleted[1] and not testRunning[1]:
testRunning[1] = true
log "Test 2 started."
log "This test validates the DataTransfer and DataTransferItem objects passed as part of the drag events."
var isChrome = ($navigator.userAgent).contains("Chrome")
log "[DT] effectAllowed: " & $dt.effectAllowed
var expectedEffectAllowed = $Uninitialized
if isChrome:
expectedEffectAllowed = if test2ImageDrop: $DataTransferEffectAllowed.Copy else: $DataTransferEffectAllowed.All
check(dt.effectAllowed == $expectedEffectAllowed, "Unexpected effectAllowed value.")
if not test2ImageDrop:
log "[DT] dropEffect: " & $dt.dropEffect
var expectedDropEffect = if isChrome: $DataTransferDropEffect.None else: $DataTransferDropEffect.Move
check(dt.dropEffect == $expectedDropEffect, "Unexpected dropEffect value.")
var expectedFilesLen = if test2ImageDrop: 1 else: 0
log "[DT] files len: " & $dt.files.len
check(dt.files.len == expectedFilesLen, "Unexpected files length.")
var expectedItemsLen = 1
log "[DT] items len: " & $dt.items.len
check(dt.items.len == expectedItemsLen, "Unexpected items length.")
var expectedItemKind = if test2ImageDrop: $DataTransferItemKind.File else: $DataTransferItemKind.String
log "[DT] items[0].kind: " & $dt.items[0].kind
check(dt.items[0].kind == expectedItemKind, "Unexpected item kind.")
var expectedItemType = if test2ImageDrop: "image/png" else: "text/plain"
log "[DT] items[0].type: " & $dt.items[0].`type`
check(dt.items[0].`type` == expectedItemType, "Unexpected item type.")
var expectedTypesLen = if not isChrome and test2ImageDrop: 2 else: 1
log "[DT] types len: " & $dt.types.len
check(dt.types.len == expectedTypesLen, "Unexpected types length.")
if test2ImageDrop:
log "[DT] types[0]: " & $dt.types[0]
if isChrome:
check(dt.types[0] == "Files", "Unexpected type value.")
else:
log "[DT] types[1]: " & $dt.types[1]
check(dt.types[0] == "application/x-moz-file", "Unexpected type value.")
check(dt.types[1] == "Files", "Unexpected type value.")
if test2ImageDrop:
test2ImageDrop = false
# TODO: Determine why getAsString() does invoke callback
# proc getAsString*(dti: DataTransferItem, callback: proc(data: cstring))
var jfile = dt.items[0].getAsFile()
log "[DT] file name: " & $jfile.name
check(jfile.name.len > 0, "Unexpected file name length.")
testCompleted[1] = true
testRunning[1] = false
log "Test 2 completed successfully."
test3()
ev.preventDefault()
return
document.getElementById("dragDiv").style.visibility = "hidden"
log "To continue, drag a .PNG file (file type matters) anywhere on this page."
log "NOTE: If the browser redirects to the image, then consider this test as failed."
test2ImageDrop = true
testRunning[1] = false
ev.preventDefault()
proc dragDivDrag(ev: kdom.Event) =
dragEventsCompleted[Drag] = true
ev.preventDefault()
proc dragDivDragLeave(ev: kdom.Event) =
dragEventsCompleted[DragLeave] = true
if testRunning[0]:
log "[dragLeave]"
ev.preventDefault()
proc dragDivDragExit(ev: kdom.Event) =
dragEventsCompleted[DragExit] = true
if testRunning[0]:
log "[dragExit]"
ev.preventDefault()
proc dragDivDragEnd(ev: kdom.Event) =
dragEventsCompleted[DragEnd] = true
if testRunning[0]:
log "[dragEnd]"
ev.preventDefault()
proc dragDivDragEnter(ev: kdom.Event) =
dragEventsCompleted[DragEnter] = true
if testRunning[0]:
log "[dragEnter]"
ev.preventDefault()
proc initDragEvents() =
log "Drag event initialization started."
var dragDiv = document.getElementById("dragDiv")
document.addEventListener($DragEventTypes.DragOver, dragDivDragOver, false)
document.addEventListener($DragEventTypes.Drop, dragDivDrop, false)
document.addEventListener($DragEventTypes.Drag, dragDivDrag, false)
dragDiv.addEventListener($DragEventTypes.DragStart, dragDivDragStart, false)
dragDiv.addEventListener($DragEventTypes.DragExit, dragDivDragExit, false)
dragDiv.addEventListener($DragEventTypes.DragLeave, dragDivDragLeave, false)
dragDiv.addEventListener($DragEventTypes.DragEnd, dragDivDragEnd, false)
dragDiv.addEventListener($DragEventTypes.DragEnter, dragDivDragEnter, false)
log "Drag event initialization complete."
log "Drag the div to continue the test. Make sure you drag the div over another element."
log "Then click the \"Check test 1 status\" button."
proc initMouseEvents() =
log "Mouse event initialization started."
document.addEventListener($DomEvent.MouseDown, mouseDown, false)
document.addEventListener($DomEvent.MouseMove, mouseMove, false)
log "Mouse event initialization complete."
proc initKbEvents() =
log "KB event initialization started."
document.addEventListener($DomEvent.KeyDown, keyDown, false)
log "KB event initialization complete."
proc startTests() =
log "[Disclaimer: these tests have been tested successfully on Linux with Firefox and Chromium. Your results may vary.]"
log "Note: these tests require some manual steps, which will be shown here."
log "Note: new entries appear in this text area at the top, so read in this direction."
log " | | | | | | | | | | | | | | | | | | | | | |"
log " / \\ / \\ / \\ / \\ / \\ / \\ / \\ / \\ / \\ / \\ / \\"
log "Test 1 started."
log "This test checks that drag events are fired."
testRunning[0] = true
for evt in [Drag, DragEnd, DragEnter, DragExit, DragLeave, DragOver, DragStart, Drop]:
dragEventsCompleted.add(evt, false)
initKbEvents()
initMouseEvents()
initDragEvents()
proc checkTest1Status() =
var evtsCompleted = true
# Chrome doesn't seem to send DragExit, so ignore this
if (($navigator.userAgent).contains("Chrome")):
dragEventsCompleted[DragExit] = true
for evt, completed in dragEventsCompleted:
if not completed:
evtsCompleted = false
break
if evtsCompleted:
testRunning[0] = false
log "Test 1 completed successfully. Drag the div anywhere to start the next test."
testCompleted[0] = true
document.getElementById("checkTest1").style.visibility = "hidden"
else:
log "Test 1 has not completed yet. Make sure you drag the div over another element."
log "Status:"
for evt, completed in dragEventsCompleted:
log $evt & ": " & $completed
proc createDom(): VNode =
result = buildHtml(tdiv()):
tdiv(id="dragDiv", draggable="true"):
tdiv(class="divcontent"):
text "draggable div"
tdiv(id="results"):
for i in countdown(results.len - 1, 0):
tdiv:
text $results[i]
tdiv(id="checkTest1"):
button(onclick=checkTest1Status):
text "Check test 1 status"
input(`type`="hidden", id="test9value")
input(`type`="text", id="test10input1")
input(`type`="text", id="test10input2")
h3(id="test11h3"):
text "test 11"
input(`type`="text", id="test12input")
proc onload() =
startTests()
setRenderer createDom
onload()

10253
tests/jquery-3.2.1.js vendored

File diff suppressed because it is too large Load diff

View file

@ -1,18 +0,0 @@
import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils
var contents: seq[cstring] = @[]
proc onTodoEnter(e: Event; n: VNode) =
contents.insert(n.value & "BBBB")
contents.insert(n.value)
n.value = ""
proc createDom(): VNode =
result = buildHtml(tdiv):
input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
onkeyupenter = onTodoEnter, setFocus)
for c in contents:
tdiv:
text c
setRenderer createDom

View file

@ -1,27 +0,0 @@
## Simple test that shows Karax can also do client-side HTML rendering.
import "../karax" / [karaxdsl, vdom]
when defined(js):
{.error: "Use 'nim c' to compile this example".}
template kxi(): int = 0
template addEventHandler(n: VNode; k: EventKind; action: string; kxi: int) =
n.setAttr($k, action)
let tab = buildHtml(table):
tr:
td:
text "Cell A"
td:
text "Cell B"
tr:
td:
text "Cell C"
td:
text "Cell D"
td:
a(href = "#/", onclick = "javascript:myFunc()"):
text"haha"
echo tab

View file

@ -1 +0,0 @@
--path: "../karax"

View file

@ -1,22 +0,0 @@
# For now we only test that the things still compile.
import os
proc exec(cmd: string) =
if os.execShellCmd(cmd) != 0:
quit "command failed " & cmd
proc main =
exec("nim js tests/diffDomTests.nim")
exec("nim js tests/compiler_tests.nim")
exec("nim js examples/todoapp/todoapp.nim")
exec("nim js examples/scrollapp/scrollapp.nim")
exec("nim js examples/mediaplayer/playerapp.nim")
exec("nim js examples/carousel/carousel.nim")
exec("nim js -d:nodejs -r tests/difftest.nim")
exec("nim c tests/nativehtmlgen.nim")
for test in os.walkFiles("examples/*.nim"):
exec("nim js " & test)
main()

16
uibench/index.html Normal file
View file

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="https://localvoid.github.io/uibench-base/0.1.0/styles.css" rel="stylesheet" />
<title>UI Benchmark: React</title>
</head>
<body>
<div id="ROOT"></div>
<script src="https://localvoid.github.io/uibench-base/0.1.0/uibench.js"></script>
<script src="nimcache/nim_uibench.js" type="text/javascript"></script>
</body>
</html>

105
uibench/nim_uibench.nim Normal file
View file

@ -0,0 +1,105 @@
import dom, vdom, karax, karaxdsl, jdict, jstrutils, kajax
type
HomeState {.importc.} = ref object
TableItemState {.importc.} = ref object
id: int
active: bool
props: seq[cstring]
TableState {.importc.} = ref object
items: seq[TableItemState]
AnimBoxState {.importc.} = ref object
id: int
time: int
AnimState {.importc.} = ref object
items: seq[AnimBoxState]
TreeNodeState {.importc.} = ref object
id: int
container: bool
children: seq[TreeNodeState]
TreeState {.importc.} = ref object
root: TreeNodeState
AppState {.importc.} = ref object
location: cstring
home: HomeState
table: TableState
anim: AnimState
tree: TreeState
var appState: AppState
proc createTableCell(id: cstring): VNode =
proc tableCellClick(ev: karax.Event; n: VNode) =
kout "Clicked" & id
result = buildHtml(td(class="TableCell", onclick=tableCellClick)):
text id
proc createTableRow(item: TableItemState): VNode =
let className = if item.active: cstring"TableRow active" else: cstring"TableRow"
result = buildHtml(tr(class=className, `data-id` = &item.id)):
createTableCell("#" & &item.id)
for i in 0..<len(item.props):
createTableCell(item.props[i])
proc tableCreateVNode(data: TableState): VNode =
result = buildHtml(table(class="Table")):
tbody:
for child in data.items:
createTableRow(child)
proc createAnimBox(item: AnimBoxState): VNode =
let time = item.time
let t10 = time mod 10
let color = float(t10) / 10
let divStyles = cstring"border-radius: " & &t10 &
"px; background: rgba(0,0,0," & cstring($color) & ")"
result = flatHtml(tdiv(class="AnimBox", `data-id` = &item.id, style=divStyles))
proc animCreateVNode(data: AnimState): VNode =
result = buildHtml(tdiv(class="Anim")):
for child in data.items:
createAnimBox(child)
proc createTreeNode(data: TreeNodeState): VNode =
result = buildHtml(ul(class="TreeNode")):
for n in data.children:
if n.container:
createTreeNode(n)
else:
li(class="TreeLeaf"):
text &n.id
proc treeCreateVNode(data: TreeState): VNode =
result = buildHtml(tdiv(class="Tree")):
createTreeNode(data.root)
proc update(): VNode =
assert appState != nil
let location = appState.location
var children: VNode = nil
if location == cstring"table":
children = tableCreateVNode(appState.table)
elif location == cstring"anim":
children = animCreateVNode(appState.anim)
elif location == cstring"tree":
children = treeCreateVNode(appState.tree)
result = buildHtml():
tdiv(class="Main"):
children
proc a(state: AppState) =
appState = state
redrawForce()
proc myToJson[T](x: T): cstring {.importcpp: "JSON.stringify(#, null, ' ')".}
proc b(samples: RootRef) =
document.body.innerHTML = cstring"<pre>" & myToJson(samples) & cstring"</pre>"
proc init*(a: cstring, b: cstring) {.importc: "uibench.init", nodecl.}
proc run*(a: proc(state: AppState), b: proc(samples: RootRef)) {.importc: "uibench.run", nodecl.}
init(cstring"Nim-karax", cstring"0.6.1")
setRendererOnly update
run(a, b)

1
uibench/nim_uibench.nims Normal file
View file

@ -0,0 +1 @@
--path: "../src"