Compare commits

...

31 commits

Author SHA1 Message Date
geotre
85c81d8c0e
Fix issue #195 (#196) 2021-05-15 21:22:56 +02:00
geotre
6d51d7d08c
Fix issue #192 (#197) 2021-05-15 21:22:33 +02:00
Arham Jain
93d9b73867
Moved CI from Travis to Github Actions (#198)
* Create ci.yml

* removed travis build file

* changed docker image

* Update readme to include CI results
2021-05-06 22:56:55 +02:00
Andreas Rumpf
ac210fb634
Update readme.md 2021-05-06 17:57:48 +02:00
Willyboar
df828e145d
beautify README (#194)
* beautify README

* Fix small issues

* Revert "Fix small issues"

This reverts commit 2a480addd7e2a55faaa95dd2145a6dc0aa4b95f7.

* Small Fixes vol.2

* Fix licence link

Co-authored-by: codewski <devon@codewski.com>
Co-authored-by: ΝΙΚΟΣ <nikos@Macintosh.local>
2021-05-06 17:57:08 +02:00
Arham Jain
cedd39a65f
Remove kout deprecation (#193) 2021-05-03 10:26:15 +02:00
Matt Haggard
12b98b2338
Make 'karun -w -r ...' do watching for more than one iteration (#190) 2021-03-31 11:43:43 +02:00
flywind
8a7f05ac2a
Update readme.rst (#184) 2021-02-09 11:55:46 +01:00
Antonis Geralis
0fca6e961d
add all svg elements acording to the standard (#178) 2021-02-04 19:24:19 +01:00
Double_oxygeN
32de202845
add picture tag as a kind of virtual DOM (#174) 2021-01-25 07:51:24 +01:00
Antonis Geralis
6cff2d8b05
Refactor static_server (#169) 2021-01-08 17:07:19 +01:00
Araq
4c06cb3204 1.2.1 2020-12-08 18:03:14 +01:00
Araq
9a627985d0 added a getInputText API, cleaner and more robust than the .text attribute which is often updated too late in event handlers after a redraw 2020-12-08 18:02:59 +01:00
Constantine Molchanov
24a037ad19
Get rid of annoying deprecation warnings (#164)
* Get rid of annoying deprecation warnings

Changed existsFile to fileExists and existsDir to dirExists.

* Install packages during nimble develop
2020-11-25 11:28:12 +01:00
Xie Yanbo
40daae64e3
fix typo (#163) 2020-11-02 08:17:27 +01:00
Xie Yanbo
d3fc084c42
added documentation on rendering to string when using event handlers (#162) 2020-10-30 10:44:38 +01:00
Demian Florentin
bcab551f05
onwheel event added (#161) 2020-10-22 11:38:22 +02:00
Timothee Cour
04779f8502
add toCss: allows tdiv(style = "font-style: oblique; color: pink".toCss) (#158)
* add toCss: allows tdiv(style = "font-style: oblique; color: pink".toCss)

* fixup to work with nim c
2020-10-07 12:23:16 +02:00
Timothee Cour
bd18fc980c
fix #153 (#159) 2020-10-01 22:13:54 +02:00
Araq
5a2ea1eef9 1.1.3 2020-10-01 19:56:30 +02:00
Timothee Cour
d2f88b4648
close #6 (#157) 2020-10-01 19:35:25 +02:00
Timothee Cour
d8fe79fa40
make vstyles.setAttr kstring overload public (#156) 2020-10-01 09:28:42 +02:00
OctoCat
f0dd16ebfe
Fixed the error with password (#155)
* Fixed the error with password

* Changed to old style
2020-09-28 08:46:23 +02:00
Bung
0813a7adf9
add transition events (#151) 2020-08-26 18:03:52 +02:00
Bung
7a36b19729
watch ignore precompile style lang (#150) 2020-07-31 17:54:05 +02:00
Bung
98a2d1ce50
add static server,websocket refresh watching mode (#149)
* add static server,websocket refresh watching mode

* watch optional

* we dont quit when building error in watch mode

* staticDir,publicUrl configure through dot env

* remove unused import

* serve optional
2020-07-23 10:40:04 +02:00
tarunama
22e2e226cb
Refactor examples html, nim files. (#147)
* refactor: remove no need first new line

* arrange: add indent in HTML files

Co-authored-by: Takuya Yamamoto <takuya.yamamoto@tabian.co>
2020-07-02 10:36:20 +02:00
dawkot
f1cd9e606d
Added toSelected (#145)
Co-authored-by: dawkot <dawkot@github.com>
2020-05-02 13:42:53 +02:00
Miran
80f500874b
fix deprecated stuff (#144) 2020-04-27 10:59:54 +02:00
Araq
5f21dcdc56 1.1.2 2020-04-12 23:41:55 +02:00
Araq
02038d40c1 fixes #141 2020-04-12 23:41:24 +02:00
22 changed files with 784 additions and 475 deletions

31
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,31 @@
# 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,3 +3,5 @@ nimcache/
karax/tools/karun karax/tools/karun
*.code-workspace *.code-workspace
*.exe *.exe
app.js
app.html

View file

@ -1,20 +0,0 @@
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

@ -1,4 +1,3 @@
include karax / prelude include karax / prelude
var lines: seq[kstring] = @[] var lines: seq[kstring] = @[]

17
examples/hellostyle.nim Normal file
View file

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

View file

@ -1,4 +1,3 @@
include karax / prelude include karax / prelude
from sugar import `=>` from sugar import `=>`
import karax / errors import karax / errors
@ -18,7 +17,7 @@ const
proc validateNotEmpty(field: kstring): proc () = proc validateNotEmpty(field: kstring): proc () =
result = proc () = result = proc () =
let x = getVNodeById(field) let x = getVNodeById(field)
if x.text.isNil or x.text == "": if x.text == "":
errors.setError(field, field & " must not be empty") errors.setError(field, field & " must not be empty")
else: else:
errors.setError(field, "") errors.setError(field, "")

View file

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

View file

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

View file

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

View file

@ -7,9 +7,8 @@ export kdom.Event, kdom.Blob
when defined(nimNoNil): when defined(nimNoNil):
{.experimental: "notnil".} {.experimental: "notnil".}
proc kout*[T](x: T) {.importc: "console.log", varargs, deprecated.} proc kout*[T](x: T) {.importc: "console.log", varargs.}
## the preferred way of debugging karax applications. Now deprecated, ## The preferred way of debugging karax applications.
## you can now use ``system.echo`` instead.
type type
PatchKind = enum PatchKind = enum
@ -38,6 +37,7 @@ type
toFocus: Node toFocus: Node
toFocusV: VNode toFocusV: VNode
renderId: int renderId: int
rendering: bool
patches: seq[Patch] # we reuse this to save allocations patches: seq[Patch] # we reuse this to save allocations
patchLen: int patchLen: int
patchesV: seq[PatchV] patchesV: seq[PatchV]
@ -154,7 +154,9 @@ proc getVNodeById*(id: cstring; kxi: KaraxInstance = kxi): VNode =
proc toDom*(n: VNode; useAttachedNode: bool; kxi: KaraxInstance = nil): Node = proc toDom*(n: VNode; useAttachedNode: bool; kxi: KaraxInstance = nil): Node =
if useAttachedNode: if useAttachedNode:
if n.dom != nil: return n.dom if n.dom != nil:
if n.id != nil: kxi.byId[n.id] = n
return n.dom
if n.kind == VNodeKind.text: if n.kind == VNodeKind.text:
result = document.createTextNode(n.text) result = document.createTextNode(n.text)
attach n attach n
@ -383,6 +385,8 @@ proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) =
proc moveDom(dest, src: VNode) = proc moveDom(dest, src: VNode) =
dest.dom = src.dom dest.dom = src.dom
src.dom = nil src.dom = nil
if dest.id != nil:
kxi.byId[dest.id] = dest
assert dest.len == src.len assert dest.len == src.len
for i in 0..<dest.len: for i in 0..<dest.len:
moveDom(dest[i], src[i]) moveDom(dest[i], src[i])
@ -626,8 +630,21 @@ proc avoidDomDiffing*(kxi: KaraxInstance = kxi) =
## This is an experimental API. ## This is an experimental API.
kxi.currentTree = nil kxi.currentTree = nil
proc reqFrame(callback: proc()): int {.importc: "window.requestAnimationFrame".}
when false:
proc cancelFrame(id: int) {.importc: "window.cancelAnimationFrame".}
proc dodraw(kxi: KaraxInstance) = proc dodraw(kxi: KaraxInstance) =
if kxi.renderer.isNil: return 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 rdata = RouterData(hashPart: hashPart)
let newtree = kxi.renderer(rdata) let newtree = kxi.renderer(rdata)
inc kxi.runCount inc kxi.runCount
@ -658,16 +675,12 @@ proc dodraw(kxi: KaraxInstance) =
# now that it's part of the DOM, give it the focus: # now that it's part of the DOM, give it the focus:
if kxi.toFocus != nil: if kxi.toFocus != nil:
kxi.toFocus.focus() kxi.toFocus.focus()
kxi.renderId = 0 kxi.rendering = false
when defined(stats): when defined(stats):
kxi.recursion = 0 kxi.recursion = 0
var total = 0 var total = 0
echo "depth ", depth(kxi.currentTree, total), " total ", total 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) = proc redraw*(kxi: KaraxInstance = kxi) =
# we buffer redraw requests: # we buffer redraw requests:
when false: when false:
@ -787,11 +800,10 @@ proc prepend(parent, kid: Element) =
parent.insertBefore(kid, parent.firstChild) parent.insertBefore(kid, parent.firstChild)
proc loadScript*(jsfilename: cstring; kxi: KaraxInstance = kxi) = proc loadScript*(jsfilename: cstring; kxi: KaraxInstance = kxi) =
let body = document.getElementById("body")
let s = document.createElement("script") let s = document.createElement("script")
s.setAttr "type", "text/javascript" s.setAttr "type", "text/javascript"
s.setAttr "src", jsfilename s.setAttr "src", jsfilename
body.prepend(s) document.body.prepend(s)
redraw(kxi) redraw(kxi)
proc runLater*(action: proc(); later = 400): Timeout {.discardable.} = proc runLater*(action: proc(); later = 400): Timeout {.discardable.} =
@ -805,11 +817,18 @@ proc setInputText*(n: VNode; s: cstring) =
n.text = s n.text = s
if n.dom != nil: n.dom.value = 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 = proc toChecked*(checked: bool): cstring =
(if checked: cstring"checked" else: cstring(nil)) (if checked: cstring"checked" else: cstring(nil))
proc toDisabled*(disabled: bool): cstring = proc toDisabled*(disabled: bool): cstring =
(if disabled: cstring"disabled" else: cstring(nil)) (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 = proc vnodeToDom*(n: VNode; kxi: KaraxInstance = nil): Node =
result = toDom(n, useAttachedNode = false, kxi) result = toDom(n, useAttachedNode = false, kxi)

View file

@ -1,21 +1,16 @@
## 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 const
css = """ 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.7.4/css/bulma.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
""" """
html = """
const html = """
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@ -23,33 +18,81 @@ const
<title>$1</title> <title>$1</title>
$2 $2
</head> </head>
<body id="body"> <body id="body" class="site">
<div id="ROOT" /> <div id="ROOT"></div>
<script type="text/javascript" src="$1.js"></script> <script type="text/javascript" src="/app.js"></script>
$3
</body> </body>
</html> </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) = proc exec(cmd: string) =
if os.execShellCmd(cmd) != 0: if os.execShellCmd(cmd) != 0:
quit "External command failed: " & cmd quit "External command failed: " & cmd
proc build(name: string, rest: string, selectedCss: string, run: bool) = proc build(rest: string, selectedCss: string, run: bool, watch: bool) =
echo("Building...") echo("Building...")
exec("nim js --out:" & name & ".js " & rest) let cmd = "nim js --out:" & "app" & ".js " & rest
let dest = name & ".html" if watch:
writeFile(dest, html % [name, selectedCss]) discard os.execShellCmd(cmd)
if run: openDefaultBrowser(dest) 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 = proc main =
var op = initOptParser() var op = initOptParser()
var rest = op.cmdLineRest var rest = op.cmdLineRest
var file = "" var file = ""
var run = false var run = false
var selectedCss = ""
var watch = false var watch = false
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable var selectedCss = ""
while true: while true:
op.next() op.next()
case op.kind case op.kind
@ -59,8 +102,11 @@ proc main =
run = true run = true
rest = rest.replace("--run ") rest = rest.replace("--run ")
of "css": of "css":
if op.val != "":
selectedCss = readFile(op.val)
else:
selectedCss = css selectedCss = css
rest = rest.replace("--css ") rest = rest.substr(rest.find(" "))
else: discard else: discard
of cmdShortOption: of cmdShortOption:
if op.key == "r": if op.key == "r":
@ -73,25 +119,11 @@ proc main =
of cmdEnd: break of cmdEnd: break
if file.len == 0: quit "filename expected" if file.len == 0: quit "filename expected"
let name = file.splitFile.name if run:
build(name, rest, selectedCss, run) spawn serve()
if watch: if watch:
# TODO: launch http server spawn watchBuild(file, selectedCss, rest)
while true: build(rest,selectedCss, run, watch)
sleep(300) sync()
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() main()

1
karax/tools/karun.nims Normal file
View file

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

View file

@ -0,0 +1,141 @@
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

@ -35,7 +35,19 @@ type
mark, ruby, rt, rp, bdi, dbo, span, br, wbr, mark, ruby, rt, rp, bdi, dbo, span, br, wbr,
ins, del, img, iframe, embed, `object` = "object", ins, del, img, iframe, embed, `object` = "object",
param, video, audio, source, track, canvas, map, param, video, audio, source, track, canvas, map,
area, svg, math, path, circle 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, table, caption, colgroup, col, tbody, thead,
tfoot, tr, td, th, tfoot, tr, td, th,
@ -97,6 +109,13 @@ type
## passed (useful for on-the-fly text completions) ## passed (useful for on-the-fly text completions)
onload, # img onload, # img
ontransitioncancel,
ontransitionend,
ontransitionrun,
ontransitionstart,
onwheel # fires when the user rotates a wheel button on a pointing device.
macro buildLookupTables(): untyped = macro buildLookupTables(): untyped =
var a = newTree(nnkBracket) var a = newTree(nnkBracket)
for i in low(VNodeKind)..high(VNodeKind): for i in low(VNodeKind)..high(VNodeKind):
@ -298,6 +317,12 @@ proc sameAttrs*(a, b: VNode): bool =
proc addEventListener*(n: VNode; event: EventKind; handler: EventHandler) = proc addEventListener*(n: VNode; event: EventKind; handler: EventHandler) =
n.events.add((event, handler, nil)) 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) = template toStringAttr(field) =
if n.field.len > 0: if n.field.len > 0:
result.add " " & astToStr(field) & " = " & $n.field result.add " " & astToStr(field) & " = " & $n.field

View file

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

394
readme.md Normal file
View file

@ -0,0 +1,394 @@
![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).

View file

@ -1,355 +0,0 @@
Karax – Single page applications in Nim |travis|
================================================
Karax is a framework for developing single page applications in Nim.
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>`_.)
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.
.. code-block:: 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()