Compare commits

..

No commits in common. "master" and "1.0.0" have entirely different histories.

37 changed files with 1827 additions and 2306 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

2
.gitignore vendored
View file

@ -3,5 +3,3 @@ nimcache/
karax/tools/karun
*.code-workspace
*.exe
app.js
app.html

20
.travis.yml Normal file
View file

@ -0,0 +1,20 @@
sudo: false
language: c
os: linux
install:
- git clone https://github.com/nim-lang/nim
- cd nim
- git clone --depth 1 https://github.com/nim-lang/csources.git
- cd csources
- sh build.sh
- cd ../
- bin/nim c koch
- ./koch boot -d:release
- ./koch tools
- cd ..
before_script:
- set -e
- export PATH=$(pwd)/nim/bin:$(pwd):$PATH
script:
- nimble develop
- nim c -r tests/tester.nim

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,3 +1,4 @@
include karax / prelude
var lines: seq[kstring] = @[]

View file

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

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,9 +1,10 @@
include karax / prelude
import random
proc createDom(): VNode =
result = buildHtml(tdiv):
if rand(100) <= 50:
if random(100) <= 50:
text "Hello World!"
else:
text "Hello Universe"

View file

@ -1,5 +1,6 @@
include karax / prelude
from sugar import `=>`
from future import `=>`
import karax / errors
proc loginField(desc, field, class: kstring;
@ -17,7 +18,7 @@ const
proc validateNotEmpty(field: kstring): proc () =
result = proc () =
let x = getVNodeById(field)
if x.text == "":
if x.text.isNil or x.text == "":
errors.setError(field, field & " must not be empty")
else:
errors.setError(field, "")
@ -27,7 +28,7 @@ var loggedIn: bool
proc loginDialog(): VNode =
result = buildHtml(tdiv):
if not loggedIn:
loginField("Name: ", username, "input", validateNotEmpty)
loginField("Name :", username, "input", validateNotEmpty)
loginField("Password: ", password, "password", validateNotEmpty)
button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()):
text "Login"

View file

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

View file

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

View file

@ -1,16 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<head>
<title>Todo app</title>
<link rel="stylesheet" href="style.css">
</head>
</head>
<body id="body">
<body id="body">
<div id="ROOT" />
<div id="ROOT" />
<script type="text/javascript" src="todoapp.js"></script>
<script type="text/javascript" src="nimcache/todoapp.js"></script>
</body>
</body>
</html>

View file

@ -9,7 +9,7 @@ var
selectedEntry = -1
filter: Filter
entriesLen: int
doneswitch = true
const
contentSuffix = cstring"content"
completedSuffix = cstring"completed"
@ -62,9 +62,9 @@ proc toggleEntry(ev: Event; n: VNode) =
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)
@ -124,8 +124,8 @@ proc createDom(data: RouterData): VNode =
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

View file

@ -1,3 +1,4 @@
include login
const
@ -11,7 +12,7 @@ var allMessages: seq[TextMessage] = @[]
proc doSendMessage() =
let inputField = getVNodeById(message)
allMessages.add(TextMessage(name: "you", content: inputField.getInputText))
allMessages.add(TextMessage(name: "you", content: inputField.text))
inputField.setInputText ""
proc main(): VNode =

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

@ -10,5 +10,5 @@
<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>
<script type="text/javascript" src="nimcache/echartstest.js"></script>
</html>

View file

@ -8,6 +8,6 @@
<body id="body">
<div id="ROOT"></div>
<script type="text/javascript" src="demo.js"></script>
<script type="text/javascript" src="nimcache/demo.js"></script>
</body>
</html>

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,15 +1,14 @@
# Package
version = "1.2.1"
version = "1.0.0"
author = "Andreas Rumpf"
description = "Karax is a framework for developing single page applications in Nim."
license = "MIT"
# Dependencies
requires "nim >= 0.18.0"
requires "ws"
requires "dotenv"
requires "nim >= 0.16.1"
skipDirs = @["examples", "experiments", "tests"]
bin = @["karax/tools/karun"]

View file

@ -61,7 +61,7 @@ proc newname*(n: NimNode): NimNode =
n[1] = newname(n[1])
result = n
elif n.kind == nnkSym:
result = ident(n.strVal)
result = ident($n.symbol)
else:
result = n

View file

@ -11,7 +11,4 @@ type
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

@ -4,66 +4,6 @@
## 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;
@ -76,8 +16,20 @@ proc ajax*(meth, url: cstring; headers: openarray[(cstring, cstring)];
cont(httpStatus, response)
if doRedraw: redraw(kxi)
var this {.importc: "this".}: ThisObj
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 send(r: HttpRequest, data: Blob) {.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):
@ -129,4 +81,3 @@ proc ajaxDelete*(url: cstring; headers: openarray[(cstring, cstring)];
proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".}
proc fromJson*[T](blob: cstring): T {.importc: "JSON.parse".}

View file

@ -7,16 +7,17 @@ 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.
proc kout*[T](x: T) {.importc: "console.log", varargs, deprecated.}
## the preferred way of debugging karax applications. Now deprecated,
## you can now use ``system.echo`` instead.
type
PatchKind = enum
pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach, pkSame
pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach
Patch = object
k: PatchKind
parent, current: Node
newNode, oldNode: VNode
n: VNode
PatchV = object
parent, newChild: VNode
pos: int
@ -30,14 +31,13 @@ type
KaraxInstance* = ref object ## underlying karax instance. Usually you don't have
## know about this.
rootId: cstring
rootId: cstring not nil
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]
@ -78,24 +78,6 @@ template keyeventBody() =
# 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 =
@ -124,18 +106,18 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind;
case k
of EventKind.onkeyuplater:
result = laterWrapper()
d.addEventShell("keyup", result)
d.addEventListener("keyup", result)
of EventKind.onkeyupenter:
result = enterWrapper()
d.addEventShell("keyup", result)
d.addEventListener("keyup", result)
else:
result = stdWrapper()
d.addEventShell(toEventName[k], result)
d.addEventListener(toEventName[k], result)
# --------------------- DOM diff -----------------------------------------
template detach(n: VNode) =
addPatch(kxi, pkDetach, nil, nil, nil, n)
addPatch(kxi, pkDetach, nil, nil, n)
template attach(n: VNode) =
n.dom = result
@ -152,11 +134,7 @@ proc getVNodeById*(id: cstring; kxi: KaraxInstance = kxi): VNode =
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
proc vnodeToDom*(n: VNode; kxi: KaraxInstance = nil): Node =
if n.kind == VNodeKind.text:
result = document.createTextNode(n.text)
attach n
@ -167,7 +145,7 @@ proc toDom*(n: VNode; useAttachedNode: bool; kxi: KaraxInstance = nil): Node =
return result
elif n.kind == VNodeKind.vthunk:
let x = callThunk(vcomponents[n.text], n)
result = toDom(x, useAttachedNode, kxi)
result = vnodeToDom(x, kxi)
#n.key = result.key
attach n
return result
@ -185,14 +163,14 @@ proc toDom*(n: VNode; useAttachedNode: bool; kxi: KaraxInstance = nil): Node =
x.expanded = x.renderImpl(x)
# x.updatedImpl(x, nil)
assert x.expanded != nil
result = toDom(x.expanded, useAttachedNode, kxi)
result = vnodeToDom(x.expanded, kxi)
attach n
return result
else:
result = document.createElement(toTag[n.kind])
attach n
for k in n:
appendChild(result, toDom(k, useAttachedNode, kxi))
appendChild(result, vnodeToDom(k, kxi))
# text is mapped to 'value':
if n.text != nil:
result.value = n.text
@ -253,7 +231,7 @@ when defined(profileKarax):
for i in low(DifferEnum)..high(DifferEnum):
echo i, " value: ", a[i]
proc eq(a, b: VNode; recursive: bool): EqResult =
proc eq(a, b: VNode): EqResult =
if a.kind != b.kind:
when defined(profileKarax): inc reasons[deKind]
return different
@ -272,12 +250,9 @@ proc eq(a, b: VNode; recursive: bool): EqResult =
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
if eq(a[i], b[i]) == 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
@ -299,14 +274,6 @@ proc eq(a, b: VNode; recursive: bool): EqResult =
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
@ -331,9 +298,6 @@ proc updateAttributes(newNode, oldNode: VNode) =
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
@ -357,18 +321,16 @@ when false:
printV(n[i], depth & " ")
proc addPatch(kxi: KaraxInstance; ka: PatchKind; parenta, currenta: Node;
na, oldNode: VNode) =
na: 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))
kxi.patches.add(Patch(k: ka, parent: parenta, current: currenta, n: na))
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
kxi.patches[L].n = na
inc kxi.patchLen
proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) =
@ -382,40 +344,26 @@ proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) =
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)
let nn = vnodeToDom(p.n, 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)
let nn = vnodeToDom(p.n, kxi)
p.parent.appendChild(nn)
of pkInsertBefore:
let nn = toDom(p.newNode, useAttachedNode = true, kxi)
let nn = vnodeToDom(p.n, kxi)
p.parent.insertBefore(nn, p.current)
of pkDetach:
let n = p.oldNode
let n = p.n
if n.id != nil: kxi.byId.del(n.id)
if n.kind == VNodeKind.component:
let x = VComponent(n)
@ -429,38 +377,16 @@ proc applyPatch(kxi: KaraxInstance) =
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) =
proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): EqResult =
when defined(stats):
if kxi.recursion > 100:
echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode, false)
echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode)
if oldNode.kind == VNodeKind.text:
echo oldNode.text
#return
#doAssert false, "overflow!"
inc kxi.recursion
let result = eq(newNode, oldNode, false)
result = eq(newNode, oldNode)
case result
of componentsIdentical:
kxi.components.add ComponentPair(oldNode: VComponent(oldNode),
@ -478,54 +404,90 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance) =
if newNode.events.len != 0 or oldNode.events.len != 0:
mergeEvents(newNode, oldNode, kxi)
when false:
if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea:
if oldNode.text != newNode.text:
oldNode.text = newNode.text
oldNode.dom.value = newNode.text
let newLength = newNode.len
let oldLength = oldNode.len
if newLength == 0 and oldLength == 0: return
if newLength == 0 and oldLength == 0: return result
let minLength = min(newLength, oldLength)
assert oldNode.kind == newNode.kind
var commonPrefix = 0
let isSpecial = oldNode.kind == VNodeKind.component or
oldNode.kind == VNodeKind.vthunk or
oldNode.kind == VNodeKind.dthunk
template eqAndUpdate(a: VNode; i: int; b: VNode; j: int; info, action: untyped) =
let oldLen = kxi.patchLen
let oldLenV = kxi.patchLenV
assert i < a.len
assert j < b.len
let r = if isSpecial:
diff(a[i], b[j], parent, current, kxi)
else:
diff(a[i], b[j], current, current.childNodes[j], kxi)
case r
of identical, componentsIdentical, similar:
a[i] = b[j]
action
of usenewNode:
kxi.addPatchV(b, j, a[i])
action
# unfortunately, we need to propagate the changes upwards:
result = useNewNode
of different:
# undo what 'diff' would have done:
kxi.patchLen = oldLen
kxi.patchLenV = oldLenV
if result != different: result = r
break
# compute common prefix:
while commonPrefix < minLength:
if eq(newNode[commonPrefix], oldNode[commonPrefix], true) == identical:
kxi.addPatch(pkSame, nil, nil, newNode[commonPrefix], oldNode[commonPrefix])
eqAndUpdate(newNode, commonPrefix, oldNode, commonPrefix, cstring"prefix"):
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])
eqAndUpdate(newNode, newPos, oldNode, oldPos, cstring"suffix"):
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)
let r = diff(newNode[i], oldNode[i], current, current.childNodes[i], kxi)
if r == usenewNode:
#oldNode[i] = newNode[i]
kxi.addPatchV(oldNode, i, newNode[i])
elif r != different:
newNode[i] = oldNode[i]
#else:
# result = usenewNode
if oldPos + 1 == oldLength:
for i in pos..newPos:
kxi.addPatch(pkAppend, current, nil, newNode[i], nil)
kxi.addPatch(pkAppend, current, nil, newNode[i])
result = usenewNode
else:
let before = current.childNodes[oldPos + 1]
for i in pos..newPos:
kxi.addPatch(pkInsertBefore, current, before, newNode[i], nil)
kxi.addPatch(pkInsertBefore, current, before, newNode[i])
result = usenewNode
# 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)
kxi.addPatch(pkRemove, current, current.childNodes[i], nil)
result = usenewNode
of different:
detach(oldNode)
kxi.addPatch(pkReplace, parent, current, newNode, nil)
kxi.addPatch(pkReplace, parent, current, newNode)
of usenewNode: doAssert(false, "eq returned usenewNode")
when defined(stats):
dec kxi.recursion
@ -555,12 +517,11 @@ proc applyComponents(kxi: KaraxInstance) =
x.renderedVersion = x.version
if oldExpanded == nil:
detach(x)
kxi.addPatch(pkReplace, parent, current, x.expanded, nil)
kxi.addPatch(pkReplace, parent, current, x.expanded)
when defined(karaxDebug):
echo "Component ", newNode.text, ": old expansion didn't exist"
else:
diff(x.expanded, oldExpanded, parent, current, kxi)
when false:
let res = diff(x.expanded, oldExpanded, parent, current, kxi)
if res == usenewNode:
when defined(karaxDebug):
echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)"
@ -588,7 +549,7 @@ when defined(stats):
proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) =
detach(parent[position])
let current = parent.dom
kxi.addPatch(pkRemove, current, current.childNodes[position], nil, nil)
kxi.addPatch(pkRemove, current, current.childNodes[position], nil)
parent.delete(position)
applyPatch(kxi)
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
@ -596,11 +557,11 @@ proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) =
proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) =
let current = parent.dom
if position >= parent.len:
kxi.addPatch(pkAppend, current, nil, kid, nil)
kxi.addPatch(pkAppend, current, nil, kid)
parent.add(kid)
else:
let before = current.childNodes[position]
kxi.addPatch(pkInsertBefore, current, before, kid, nil)
kxi.addPatch(pkInsertBefore, current, before, kid)
parent.insert(kid, position)
applyPatch(kxi)
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
@ -608,7 +569,7 @@ proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) =
proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) =
let olddom = oldNode.dom
doAssert olddom != nil
diff(newNode, oldNode, nil, olddom, kxi)
discard 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:
@ -622,41 +583,21 @@ proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) =
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)
let asdom = vnodeToDom(newtree, 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)
discard diff(newtree, kxi.currentTree, nil, olddom, kxi)
#kout cstring"patch len ", patches.len
when defined(profileKarax):
echo "<<<<<<<<<<<<<<"
echa reasons
@ -675,12 +616,16 @@ proc dodraw(kxi: KaraxInstance) =
# now that it's part of the DOM, give it the focus:
if kxi.toFocus != nil:
kxi.toFocus.focus()
kxi.rendering = false
kxi.renderId = 0
when defined(stats):
kxi.recursion = 0
var total = 0
echo "depth ", depth(kxi.currentTree, total), " total ", total
proc reqFrame(callback: proc()): int {.importc: "window.requestAnimationFrame".}
when false:
proc cancelFrame(id: int) {.importc: "window.cancelAnimationFrame".}
proc redraw*(kxi: KaraxInstance = kxi) =
# we buffer redraw requests:
when false:
@ -768,7 +713,7 @@ proc addEventHandler*(n: VNode; k: EventKind; action: proc();
if not kxi.surpressRedraws: redraw(kxi)
addEventListener(n, k, wrapper)
proc setOnHashChange*(action: proc (hashPart: cstring)) {.deprecated: "use setRenderer instead".} =
proc setOnHashChange*(action: proc (hashPart: cstring)) {.deprecated.} =
## Now deprecated, instead pass a callback to ``setRenderer`` that receives
## a ``data: RouterData`` parameter.
proc wrapper() =
@ -800,10 +745,11 @@ proc prepend(parent, kid: Element) =
parent.insertBefore(kid, parent.firstChild)
proc loadScript*(jsfilename: cstring; kxi: KaraxInstance = kxi) =
let body = document.getElementById("body")
let s = document.createElement("script")
s.setAttr "type", "text/javascript"
s.setAttr "src", jsfilename
document.body.prepend(s)
body.prepend(s)
redraw(kxi)
proc runLater*(action: proc(); later = 400): Timeout {.discardable.} =
@ -817,18 +763,8 @@ proc setInputText*(n: VNode; s: cstring) =
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

@ -11,8 +11,8 @@ const
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:
@ -27,8 +27,6 @@ proc getName(n: NimNode): string =
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)
@ -126,8 +124,6 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
result.add newDotAsgn(tmp, key, newCall("style", toKstring 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"):

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,98 +1,55 @@
## Simple tool to quickly run Karax applications. Generates the HTML
## required to run a Karax app and opens it in a browser.
import os,
strutils,
parseopt,
browsers,
times,
tables
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://cdnjs.cloudflare.com/ajax/libs/bulma/0.5.3/css/bulma.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
"""
const html = """
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 id="body">
<div id="ROOT" />
<script type="text/javascript" src="$1.js"></script>
</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) =
proc build(name: string, rest: string, selectedCss: string, run: 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()
createDir("nimcache")
exec("nim js --out:nimcache/" & name & ".js " & rest)
let dest = "nimcache" / name & ".html"
writeFile(dest, html % [name, selectedCss])
if run: openDefaultBrowser(dest)
proc main =
var op = initOptParser()
var rest = op.cmdLineRest
var file = ""
var run = false
var watch = false
var selectedCss = ""
var watch = false
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
while true:
op.next()
case op.kind
@ -102,11 +59,8 @@ proc main =
run = true
rest = rest.replace("--run ")
of "css":
if op.val != "":
selectedCss = readFile(op.val)
else:
selectedCss = css
rest = rest.substr(rest.find(" "))
rest = rest.replace("--css ")
else: discard
of cmdShortOption:
if op.key == "r":
@ -119,11 +73,26 @@ proc main =
of cmdEnd: break
if file.len == 0: quit "filename expected"
if run:
spawn serve()
let name = file.splitFile.name
build(name, rest, selectedCss, run)
echo("after build")
if watch:
spawn watchBuild(file, selectedCss, rest)
build(rest,selectedCss, run, watch)
sync()
# TODO: launch http server
while true:
sleep(300)
for path in walkDirRec("."):
if ".git" in path:
continue
if files.hasKey(path):
if files[path] != getLastModificationTime(path):
echo("File changed: " & path)
build(name, rest, selectedCss, run)
files[path] = getLastModificationTime(path)
else:
files[path] = getLastModificationTime(path)
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

@ -8,7 +8,7 @@ else:
Node* = ref object
import macros, vstyles, kbase
from strutils import toUpperAscii, toLowerAscii, tokenize
from strutils import toUpperAscii
type
VNodeKind* {.pure.} = enum
@ -35,19 +35,7 @@ type
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,
area, svg, math,
table, caption, colgroup, col, tbody, thead,
tfoot, tr, td, th,
@ -57,10 +45,6 @@ type
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.
@ -105,16 +89,9 @@ type
onanimationiteration,
onkeyupenter, ## vdom extension: an input field received the ENTER key press
onkeyuplater, ## vdom extension: a key was pressed and some time
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)
@ -317,12 +294,6 @@ proc sameAttrs*(a, b: VNode): bool =
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
@ -408,8 +379,6 @@ proc add*(result: var string, n: VNode, indent = 0, indWidth = 2) =
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('<')
@ -428,17 +397,7 @@ proc add*(result: var string, n: VNode, indent = 0, indWidth = 2) =
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('"')
# XXX add style to string
if n.len > 0:
result.add('>')
if n.len > 1:
@ -463,13 +422,8 @@ proc add*(result: var string, n: VNode, indent = 0, indWidth = 2) =
result.add("</")
result.add(kind)
result.add(">")
elif n.kind in selfClosing:
result.add(" />")
else:
result.add(">")
result.add("</")
result.add(kind)
result.add(">")
result.add(" />")
proc `$`*(n: VNode): kstring =

View file

@ -1,9 +1,5 @@
##[
see examples/hellostyle.nim
]##
import std/[macros, strutils]
import kbase
import macros, kbase
when defined(js):
import kdom, jdict
@ -238,10 +234,7 @@ proc eq*(a, b: VStyle): bool =
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
proc setAttr(s: VStyle; a, value: kstring) {.noSideEffect.} =
var i = 0
while i < s.len:
if s[i] == a:
@ -250,8 +243,8 @@ proc setAttr*(s: VStyle; a, value: kstring) {.noSideEffect.} =
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):
# insertion point here:
for j in countdown(s.len-1, i, 2):
s[j] = s[j-2]
s[j-1] = s[j-3]
s[i] = a
@ -296,24 +289,6 @@ proc style*(a: StyleAttr; val: kstring): VStyle {.noSideEffect.} =
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.}

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).

327
readme.rst Normal file
View file

@ -0,0 +1,327 @@
Karax – Single page applications in Nim |travis|
================================================
Karax is a framework for developing single page applications in Nim.
The API is reasonably stable and version 1 should arrive anytime soon
now.
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.
.. |travis| image:: https://travis-ci.org/pragmagic/karax.svg?branch=master
:target: https://travis-ci.org/pragmagic/karax
Hello World
===========
The simplest Karax program looks like this:
.. code-block:: 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``:
.. code-block:: 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:
.. code-block:: nim
include karax / prelude
import random
proc createDom(): VNode =
result = buildHtml(tdiv):
if random(100) <= 50:
text "Hello World!"
else:
text "Hello Universe"
randomize()
setRenderer createDom
Produces:
.. code-block:: nim
let tmp1 = tree(VNodeKind.tdiv)
if random(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:
.. code-block:: 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:
.. code-block:: 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:
.. code-block:: 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:
.. code-block:: 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``:
.. code-block:: 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:
.. code-block:: nim
proc validateNotEmpty(field: kstring): proc () =
result = proc () =
let x = getVNodeById(field)
if x.text.isNil or x.text == "":
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:
.. code-block:: 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/pragmagic/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:
.. code-block:: 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:
.. code-block:: 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/pragmagic/karax/blob/master/examples/todoapp/todoapp.nim>`_.)

View file

@ -9,7 +9,7 @@
<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

@ -22,19 +22,15 @@ proc shortRepr(n: VNode): string =
var err = 0
proc doDiff(a, b: VNode; expected: varargs[string]) =
diff(b, a, nil, vnodeToDom(a, kxi), kxi)
var j = 0
discard diff(b, a, nil, vnodeToDom(a, kxi), kxi)
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:
let p = $kxi.patches[i].k & " " & shortRepr(kxi.patches[i].n)
if i >= expected.len:
echo "patches differ; expected nothing but got: ", p
inc err
elif p != expected[j]:
elif p != expected[i]:
echo "patches differ; expected ", expected[i], " but got: ", p
inc err
inc j
#hasDom(kxi.currentTree)
kxi.patchLen = 0

View file

@ -8,6 +8,6 @@
</head>
<body id="body">
<div id="ROOT"></div>
<script src="domEventTests.js" type="text/javascript"></script>
<script src="nimcache/domEventTests.js" type="text/javascript"></script>
</body>
</html>

View file

@ -5,10 +5,6 @@ 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:
@ -20,8 +16,5 @@ let tab = buildHtml(table):
text "Cell C"
td:
text "Cell D"
td:
a(href = "#/", onclick = "javascript:myFunc()"):
text"haha"
echo tab