move karax source code from 'src' to 'karax'; make karaxdsl work with native Nim; added jjson and jwebsockets modules

This commit is contained in:
Andreas Rumpf 2017-10-02 22:54:25 +02:00
commit 8e8ada7cd2
34 changed files with 407 additions and 203 deletions

View file

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

View file

@ -1,5 +1,5 @@
import karax, karaxdsl, vdom, kdom, compact import karax / [karax, karaxdsl, vdom, kdom, compact]
const const
Play = 0 Play = 0

View file

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

View file

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

View file

@ -1,7 +1,7 @@
## Example that shows how to accomplish an "infinitely scrolling" app. ## Example that shows how to accomplish an "infinitely scrolling" app.
include karaxprelude include karax/prelude
import jstrutils, kdom, vstyles import karax / [jstrutils, kdom, vstyles]
var entries: seq[cstring] = @[] var entries: seq[cstring] = @[]
for i in 1..500: for i in 1..500:

View file

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

View file

@ -1,5 +1,5 @@
import vdom, karax, karaxdsl, jstrutils, compact, localstorage import karax / [vdom, karax, karaxdsl, jstrutils, compact, localstorage]
type type
Filter = enum Filter = enum

View file

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

View file

@ -1,13 +1,12 @@
# Package # Package
version = "0.1.0" version = "0.2.0"
author = "Xored Software, Inc." author = "Xored Software, Inc."
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"
srcDir = "src"
# Dependencies # Dependencies
requires "nim >= 0.16.1" requires "nim >= 0.16.1"
skipDirs = @["examples/todoapp"] skipDirs = @["examples"]

130
karax/compact.nim Normal file
View file

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

33
karax/jjson.nim Normal file
View file

@ -0,0 +1,33 @@
## This module implements some small zero-overhead 'JsonNode' type
## and helper that maps directly to JavaScript objects.
type
JsonNode* {.importc.} = ref object
proc `[]`*(obj: JsonNode; fieldname: cstring): JsonNode {.importcpp: "#[#]".}
proc `[]`*(obj: JsonNode; index: int): JsonNode {.importcpp: "#[#]".}
proc `[]=`*[T](obj: JsonNode; fieldname: cstring; value: T)
{.importcpp: "#[#] = #".}
proc length(x: JsonNode): int {.importcpp: "#.length".}
proc len*(x: JsonNode): int = (if x.isNil: 0 else: x.length)
proc newJsonNode*(fields: varargs[(cstring, JsonNode)]): JsonNode =
result = JsonNode()
for f in fields:
result[f[0]] = f[1]
proc newJObject*(): JsonNode =
result = JsonNode()
proc newJNull*(): JsonNode = nil
template `%`*(x: typed): JsonNode = cast[JsonNode](x)
proc getNum*(x: JsonNode): int {.importcpp: "#".}
proc getInt*(x: JsonNode): int {.importcpp: "#".}
proc getStr*(x: JsonNode): cstring {.importcpp: "#".}
proc getFNum*(x: JsonNode): cstring {.importcpp: "#".}
iterator items*(x: JsonNode): JsonNode =
for i in 0..<len(x): yield x[i]

13
karax/jwebsockets.nim Normal file
View file

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

View file

@ -143,7 +143,6 @@ proc vnodeToDom*(n: VNode; kxi: KaraxInstance): Node =
return result return result
elif n.kind == VNodeKind.component: elif n.kind == VNodeKind.component:
let x = VComponent(n) let x = VComponent(n)
if x.realDomImpl != nil: return x.realDomImpl(x)
if x.onAttachImpl != nil: x.onAttachImpl(x) if x.onAttachImpl != nil: x.onAttachImpl(x)
assert x.renderImpl != nil assert x.renderImpl != nil
if x.expanded == nil: if x.expanded == nil:
@ -479,11 +478,7 @@ proc applyComponents(kxi: KaraxInstance) =
let newNode = kxi.components[i].newNode let newNode = kxi.components[i].newNode
when defined(karaxDebug): when defined(karaxDebug):
echo "Processing component ", newNode.text, " changed impl set ", x.changedImpl != nil echo "Processing component ", newNode.text, " changed impl set ", x.changedImpl != nil
if x.realDomImpl != nil: if x.changedImpl != nil and x.changedImpl(x, newNode):
let current = kxi.components[i].current
let parent = kxi.components[i].parent
kxi.addPatch(pkReplace, parent, current, x)
elif x.changedImpl != nil and x.changedImpl(x, newNode):
when defined(karaxDebug): when defined(karaxDebug):
echo "Component ", newNode.text, " did change" echo "Component ", newNode.text, " did change"
let current = kxi.components[i].current let current = kxi.components[i].current

View file

@ -1,7 +1,10 @@
import macros, karax, vdom, compact import macros, vdom, compact
from strutils import startsWith, toLowerAscii from strutils import startsWith, toLowerAscii
when defined(js):
import karax
const const
StmtContext = ["kout", "inc", "echo", "dec", "!"] StmtContext = ["kout", "inc", "echo", "dec", "!"]
SpecialAttrs = ["id", "class", "value", "index", "style"] SpecialAttrs = ["id", "class", "value", "index", "style"]
@ -32,6 +35,11 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
# (except for the last child of the macros we consider here), # (except for the last child of the macros we consider here),
# lets, consts, types can be considered as expressions # lets, consts, types can be considered as expressions
# case is complex, calls are assumed to produce a value. # case is complex, calls are assumed to produce a value.
when defined(js):
template evHandler(): untyped = bindSym"addEventHandler"
else:
template evHandler(): untyped = ident"addEventHandler"
case n.kind case n.kind
of nnkLiterals, nnkIdent, nnkSym, nnkDotExpr, nnkBracketExpr: of nnkLiterals, nnkIdent, nnkSym, nnkDotExpr, nnkBracketExpr:
if tmpContext != nil: if tmpContext != nil:
@ -68,7 +76,7 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
if tmpContext == nil: if tmpContext == nil:
error "no VNode to attach the event handler to" error "no VNode to attach the event handler to"
else: else:
result = newCall(bindSym"addEventHandler", tmpContext, result = newCall(evHandler(), tmpContext,
newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi")) newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi"))
else: else:
result = n result = n
@ -94,7 +102,7 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
if x.kind == nnkExprEqExpr: if x.kind == nnkExprEqExpr:
let key = getName x[0] let key = getName x[0]
if key.startsWith("on"): if key.startsWith("on"):
result.add newCall(bindSym"addEventHandler", result.add newCall(evHandler(),
tmp, newDotExpr(bindSym"EventKind", x[0]), x[1], ident("kxi")) tmp, newDotExpr(bindSym"EventKind", x[0]), x[1], ident("kxi"))
elif key in SpecialAttrs: elif key in SpecialAttrs:
result.add newDotAsgn(tmp, key, x[1]) result.add newDotAsgn(tmp, key, x[1])

8
karax/kbase.nim Normal file
View file

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

View file

@ -1,7 +1,13 @@
## Virtual DOM implementation for Karax. ## Virtual DOM implementation for Karax.
from kdom import Event, Node when defined(js):
import shash, macros, vstyles from kdom import Event, Node
else:
type
Event* = ref object
Node* = ref object
import macros, vstyles, kbase
from strutils import toUpperAscii from strutils import toUpperAscii
type type
@ -88,15 +94,15 @@ macro buildLookupTables(): untyped =
for i in low(VNodeKind)..high(VNodeKind): for i in low(VNodeKind)..high(VNodeKind):
let x = $i let x = $i
let y = if x[0] == '#': x else: toUpperAscii(x) let y = if x[0] == '#': x else: toUpperAscii(x)
a.add(newCall("cstring", newLit(y))) a.add(newCall("kstring", newLit(y)))
var e = newTree(nnkBracket) var e = newTree(nnkBracket)
for i in low(EventKind)..high(EventKind): for i in low(EventKind)..high(EventKind):
e.add(newCall("cstring", newLit(substr($i, 2)))) e.add(newCall("kstring", newLit(substr($i, 2))))
template tmpl(a, e) {.dirty.} = template tmpl(a, e) {.dirty.} =
const const
toTag*: array[VNodeKind, cstring] = a toTag*: array[VNodeKind, kstring] = a
toEventName*: array[EventKind, cstring] = e toEventName*: array[EventKind, kstring] = e
result = getAst tmpl(a, e) result = getAst tmpl(a, e)
@ -108,15 +114,15 @@ type
EventHandlers* = seq[(EventKind, EventHandler, NativeEventHandler)] EventHandlers* = seq[(EventKind, EventHandler, NativeEventHandler)]
VKey* = cstring VKey* = kstring
VNode* = ref object of RootObj VNode* = ref object of RootObj
kind*: VNodeKind kind*: VNodeKind
index*: int ## a generally useful 'index' index*: int ## a generally useful 'index'
id*, class*, text*: cstring id*, class*, text*: kstring
kids: seq[VNode] kids: seq[VNode]
# even index: key, odd index: value; done this way for memory efficiency: # even index: key, odd index: value; done this way for memory efficiency:
attrs: seq[cstring] attrs: seq[kstring]
events*: EventHandlers events*: EventHandlers
when false: when false:
hash*: Hash hash*: Hash
@ -133,7 +139,6 @@ type
updatedImpl*: proc(self, newInstance: VComponent) updatedImpl*: proc(self, newInstance: VComponent)
onAttachImpl*: proc(self: VComponent) onAttachImpl*: proc(self: VComponent)
onDetachImpl*: proc(self: VComponent) onDetachImpl*: proc(self: VComponent)
realDomImpl*: proc(self: VComponent): kdom.Node
version*: int ## Update this to trigger a redraw by karax. Usually you version*: int ## Update this to trigger a redraw by karax. Usually you
## should call 'markDirty' instead which is an alias for ## should call 'markDirty' instead which is an alias for
## 'inc version'. ## 'inc version'.
@ -143,22 +148,22 @@ type
## expanded to. ## expanded to.
debugId*: int debugId*: int
proc value*(n: VNode): cstring = n.text proc value*(n: VNode): kstring = n.text
proc `value=`*(n: VNode; v: cstring) = n.text = v proc `value=`*(n: VNode; v: kstring) = n.text = v
proc intValue*(n: VNode): int = n.index proc intValue*(n: VNode): int = n.index
proc vn*(i: int): VNode = VNode(kind: VNodeKind.int, index: i) proc vn*(i: int): VNode = VNode(kind: VNodeKind.int, index: i)
proc vn*(b: bool): VNode = VNode(kind: VNodeKind.int, index: ord(b)) proc vn*(b: bool): VNode = VNode(kind: VNodeKind.int, index: ord(b))
proc vn*(x: cstring): VNode = VNode(kind: VNodeKind.text, index: -1, text: x) proc vn*(x: kstring): VNode = VNode(kind: VNodeKind.text, index: -1, text: x)
template callThunk*(fn: typed; n: VNode): untyped = template callThunk*(fn: typed; n: VNode): untyped =
## for internal usage only. ## for internal usage only.
fn(n.kids) fn(n.kids)
proc vthunk*(name: cstring; args: varargs[VNode, vn]): VNode = proc vthunk*(name: kstring; args: varargs[VNode, vn]): VNode =
VNode(kind: VNodeKind.vthunk, text: name, index: -1, kids: @args) VNode(kind: VNodeKind.vthunk, text: name, index: -1, kids: @args)
proc dthunk*(name: cstring; args: varargs[VNode, vn]): VNode = proc dthunk*(name: kstring; args: varargs[VNode, vn]): VNode =
VNode(kind: VNodeKind.dthunk, text: name, index: -1, kids: @args) VNode(kind: VNodeKind.dthunk, text: name, index: -1, kids: @args)
proc setEventIfNoConflict(v: VNode; kind: EventKind; handler: EventHandler) = proc setEventIfNoConflict(v: VNode; kind: EventKind; handler: EventHandler) =
@ -193,7 +198,7 @@ template newComponent*[T](t: typeDesc[T];
updated: proc(self, newInstance: VComponent) = defaultUpdatedImpl): T = updated: proc(self, newInstance: VComponent) = defaultUpdatedImpl): T =
## Use this template to create new components. ## Use this template to create new components.
T(kind: VNodeKind.component, index: -1, T(kind: VNodeKind.component, index: -1,
text: cstring(astToStr(t)), renderImpl: render, text: kstring(astToStr(t)), renderImpl: render,
changedImpl: changed, updatedImpl: updated, changedImpl: changed, updatedImpl: updated,
onAttachImpl: onAttach, onDetachImpl: onDetach, onAttachImpl: onAttach, onDetachImpl: onDetach,
debugId: getDebugId()) debugId: getDebugId())
@ -202,7 +207,7 @@ template markDirty*(c: VComponent) =
## mark the component as dirty so that it is re-rendered. ## mark the component as dirty so that it is re-rendered.
inc c.version inc c.version
proc setAttr*(n: VNode; key: cstring; val: cstring = "") = proc setAttr*(n: VNode; key: kstring; val: kstring = "") =
if n.attrs.isNil: if n.attrs.isNil:
n.attrs = @[key, val] n.attrs = @[key, val]
else: else:
@ -213,7 +218,7 @@ proc setAttr*(n: VNode; key: cstring; val: cstring = "") =
n.attrs.add key n.attrs.add key
n.attrs.add val n.attrs.add val
proc getAttr*(n: VNode; key: cstring): cstring = proc getAttr*(n: VNode; key: kstring): kstring =
for i in countup(0, n.attrs.len-2, 2): for i in countup(0, n.attrs.len-2, 2):
if n.attrs[i] == key: return n.attrs[i+1] if n.attrs[i] == key: return n.attrs[i+1]
@ -237,7 +242,12 @@ proc takeOverFields*(newNode, oldNode: VNode) =
proc len*(x: VNode): int = x.kids.len proc len*(x: VNode): int = x.kids.len
proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx] proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx]
proc `[]=`*(x: VNode; idx: int; y: VNode) = x.kids[idx] = y proc `[]=`*(x: VNode; idx: int; y: VNode) = x.kids[idx] = y
proc add*(parent, kid: VNode) = parent.kids.add kid
proc add*(parent, kid: VNode) =
when not defined(js) and not defined(nimNoNil):
if parent.kids.isNil: parent.kids = @[]
parent.kids.add kid
proc delete*(parent: VNode; position: int) = proc delete*(parent: VNode; position: int) =
parent.kids.delete(position) parent.kids.delete(position)
proc insert*(parent, kid: VNode; position: int) = proc insert*(parent, kid: VNode; position: int) =
@ -248,18 +258,19 @@ proc tree*(kind: VNodeKind; kids: varargs[VNode]): VNode =
result = newVNode(kind) result = newVNode(kind)
for k in kids: result.add k for k in kids: result.add k
proc tree*(kind: VNodeKind; attrs: openarray[(cstring, cstring)]; proc tree*(kind: VNodeKind; attrs: openarray[(kstring, kstring)];
kids: varargs[VNode]): VNode = kids: varargs[VNode]): VNode =
result = tree(kind, kids) result = tree(kind, kids)
for a in attrs: result.setAttr(a[0], a[1]) for a in attrs: result.setAttr(a[0], a[1])
proc text*(s: string): VNode = VNode(kind: VNodeKind.text, text: cstring(s), index: -1) when defined(js):
proc text*(s: cstring): VNode = VNode(kind: VNodeKind.text, text: s, index: -1) proc text*(s: string): VNode = VNode(kind: VNodeKind.text, text: kstring(s), index: -1)
proc text*(s: kstring): VNode = VNode(kind: VNodeKind.text, text: s, index: -1)
iterator items*(n: VNode): VNode = iterator items*(n: VNode): VNode =
for i in 0..<n.kids.len: yield n.kids[i] for i in 0..<n.kids.len: yield n.kids[i]
iterator attrs*(n: VNode): (cstring, cstring) = iterator attrs*(n: VNode): (kstring, kstring) =
for i in countup(0, n.attrs.len-2, 2): for i in countup(0, n.attrs.len-2, 2):
yield (n.attrs[i], n.attrs[i+1]) yield (n.attrs[i], n.attrs[i+1])
@ -324,7 +335,91 @@ when false:
h &= child.hash h &= child.hash
n.hash = h n.hash = h
proc `$`*(n: VNode): cstring =
var res = "" proc add*(result: var string, n: VNode, indent = 0, indWidth = 2) =
toString(n, res, 0) ## adds the textual representation of `n` to `result`.
result = cstring(res)
proc addEscapedAttr(result: var string, s: kstring) =
# `addEscaped` alternative with less escaped characters.
# Only to be used for escaping attribute values enclosed in double quotes!
for c in items(s):
case c
of '<': result.add("&lt;")
of '>': result.add("&gt;")
of '&': result.add("&amp;")
of '"': result.add("&quot;")
else: result.add(c)
proc addEscaped(result: var string, s: kstring) =
## same as ``result.add(escape(s))``, but more efficient.
for c in items(s):
case c
of '<': result.add("&lt;")
of '>': result.add("&gt;")
of '&': result.add("&amp;")
of '"': result.add("&quot;")
of '\'': result.add("&#x27;")
of '/': result.add("&#x2F;")
else: result.add(c)
proc addIndent(result: var string, indent: int) =
result.add("\n")
for i in 1..indent: result.add(' ')
if n.kind == VNodeKind.text:
result.addEscaped(n.text)
else:
let kind = $n.kind
result.add('<')
result.add(kind)
if n.id != nil:
result.add " id=\""
result.addEscapedAttr(n.id)
result.add('"')
if n.class != nil:
result.add " class=\""
result.addEscapedAttr(n.class)
result.add('"')
for k, v in attrs(n):
result.add(' ')
result.add(k)
result.add("=\"")
result.addEscapedAttr(v)
result.add('"')
# XXX add style to string
if n.len > 0:
result.add('>')
if n.len > 1:
var noWhitespace = false
for i in 0..<n.len:
if n[i].kind == VNodeKind.text:
noWhitespace = true
break
if noWhitespace:
# for mixed leaves, we cannot output whitespace for readability,
# because this would be wrong. For example: ``a<b>b</b>`` is
# different from ``a <b>b</b>``.
for i in 0..<n.len: result.add(n[i], indent+indWidth, indWidth)
else:
for i in 0..<n.len:
result.addIndent(indent+indWidth)
result.add(n[i], indent+indWidth, indWidth)
result.addIndent(indent)
else:
result.add(n[0], indent+indWidth, indWidth)
result.add("</")
result.add(kind)
result.add(">")
else:
result.add(" />")
proc `$`*(n: VNode): kstring =
when defined(js):
var res = ""
toString(n, res, 0)
result = kstring(res)
else:
result = ""
add(result, n)

View file

@ -1,5 +1,8 @@
import kdom, macros, jdict import macros, kbase
when defined(js):
import kdom, jdict
type type
StyleAttr* {.pure.} = enum StyleAttr* {.pure.} = enum
@ -200,10 +203,10 @@ type
macro buildLookupTables(): untyped = macro buildLookupTables(): untyped =
var e = newTree(nnkBracket) var e = newTree(nnkBracket)
for i in low(StyleAttr)..high(StyleAttr): for i in low(StyleAttr)..high(StyleAttr):
e.add(newCall("cstring", newLit($i))) e.add(newCall("kstring", newLit($i)))
template tmpl(e) {.dirty.} = template tmpl(e) {.dirty.} =
const const
toStyleAttrName: array[StyleAttr, cstring] = e toStyleAttrName: array[StyleAttr, kstring] = e
result = getAst tmpl(e) result = getAst tmpl(e)
buildLookupTables() buildLookupTables()
@ -212,8 +215,14 @@ buildLookupTables()
# it's relevant. # it's relevant.
# even index: key, odd index: value; done this way for memory efficiency: # even index: key, odd index: value; done this way for memory efficiency:
type when defined(js):
VStyle* = JSeq[cstring] type
VStyle* = JSeq[cstring]
else:
type
VStyle* = ref seq[string]
proc len*(a: VStyle): int = len(a[])
proc add*(a: VStyle; x: string) = add(a[], x)
proc eq*(a, b: VStyle): bool = proc eq*(a, b: VStyle): bool =
if a.isNil: if a.isNil:
@ -225,9 +234,7 @@ 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 kout[T](x: T) {.importc: "console.log", varargs.} proc setAttr(s: VStyle; a, value: kstring) {.noSideEffect.} =
proc setAttr(s: VStyle; a, value: cstring) {.noSideEffect.} =
var i = 0 var i = 0
while i < s.len: while i < s.len:
if s[i] == a: if s[i] == a:
@ -247,11 +254,11 @@ proc setAttr(s: VStyle; a, value: cstring) {.noSideEffect.} =
s.add a s.add a
s.add value s.add value
proc setAttr*(s: VStyle; attr: StyleAttr, value: cstring) {.noSideEffect.} = proc setAttr*(s: VStyle; attr: StyleAttr, value: kstring) {.noSideEffect.} =
assert value != nil, "value must not be nil" assert value != nil, "value must not be nil"
setAttr(s, toStyleAttrName[attr], value) setAttr(s, toStyleAttrName[attr], value)
proc getAttr*(s: VStyle; attr: StyleAttr): cstring {.noSideEffect.} = proc getAttr*(s: VStyle; attr: StyleAttr): kstring {.noSideEffect.} =
## returns 'nil' if the attribute has not been set. ## returns 'nil' if the attribute has not been set.
var i = 0 var i = 0
let a = toStyleAttrName[attr] let a = toStyleAttrName[attr]
@ -262,44 +269,60 @@ proc getAttr*(s: VStyle; attr: StyleAttr): cstring {.noSideEffect.} =
return nil return nil
inc i, 2 inc i, 2
proc style*(pairs: varargs[(StyleAttr, cstring)]): VStyle {.noSideEffect.} = proc style*(pairs: varargs[(StyleAttr, kstring)]): VStyle {.noSideEffect.} =
## constructs a VStyle object from a list of (attribute, value)-pairs. ## constructs a VStyle object from a list of (attribute, value)-pairs.
result = newJSeq[cstring]() when defined(js):
result = newJSeq[cstring]()
else:
new(result)
result[] = @[]
for x in pairs: for x in pairs:
result.setAttr x[0], x[1] result.setAttr x[0], x[1]
proc style*(a: StyleAttr; val: cstring): VStyle {.noSideEffect.} = proc style*(a: StyleAttr; val: kstring): VStyle {.noSideEffect.} =
## constructs a VStyle object from a single (attribute, value)-pair. ## constructs a VStyle object from a single (attribute, value)-pair.
result = newJSeq[cstring]() when defined(js):
result = newJSeq[cstring]()
else:
new(result)
result[] = @[]
result.setAttr a, val result.setAttr a, val
proc setStyle(d: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.} when defined(js):
proc setStyle(d: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.}
proc applyStyle*(n: Node; s: VStyle) {.noSideEffect.} =
## apply the style to the real DOM node ``n``.
#n.style = Style() # optimized, this is a hotspot:
{.emit: "`n`.style = {};".}
for i in countup(0, s.len-1, 2):
n.style.setStyle(s[i], s[i+1])
proc merge*(a, b: VStyle): VStyle {.noSideEffect.} = proc merge*(a, b: VStyle): VStyle {.noSideEffect.} =
## merges two styles. ``b`` takes precedence over ``a``. ## merges two styles. ``b`` takes precedence over ``a``.
result = newJSeq[cstring]() when defined(js):
result = newJSeq[cstring]()
else:
new(result)
result[] = @[]
for i in 0..<a.len: for i in 0..<a.len:
result.add a[i] result.add a[i]
for i in countup(0, b.len-1, 2): for i in countup(0, b.len-1, 2):
setAttr(result, b[i], b[i+1]) setAttr(result, b[i], b[i+1])
proc applyStyle*(n: Node; s: VStyle) {.noSideEffect.} = iterator pairs*(s: VStyle): (kstring, kstring) {.noSideEffect.} =
## apply the style to the real DOM node ``n``.
#n.style = Style() # optimized, this is a hotspot:
{.emit: "`n`.style = {};".}
for i in countup(0, s.len-1, 2):
n.style.setStyle(s[i], s[i+1])
iterator pairs*(s: VStyle): (cstring, cstring) {.noSideEffect.} =
if s != nil: if s != nil:
for i in countup(0, s.len-1, 2): for i in countup(0, s.len-1, 2):
yield (s[i], s[i+1]) yield (s[i], s[i+1])
import jstrutils when defined(js):
import jstrutils
else:
template `&`(x: untyped): untyped = $x
proc rgb*(r, g, b: range[0..255]): cstring = proc rgb*(r, g, b: range[0..255]): kstring =
cstring"rgb(" & &r & cstring", " & &g & cstring", " & &b & cstring")" kstring"rgb(" & &r & kstring", " & &g & kstring", " & &b & kstring")"
proc rgba*(r, g, b: range[0..255], alpha: float): cstring = proc rgba*(r, g, b: range[0..255], alpha: float): kstring =
cstring"rgba(" & &r & cstring", " & &g & cstring", " & &b & cstring", " & &alpha & cstring")" kstring"rgba(" & &r & kstring", " & &g & kstring", " & &b & kstring", " & &alpha & kstring")"

View file

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

View file

@ -1,4 +1,4 @@
include karaxprelude include ../karax/prelude
block: block:
type type

View file

@ -1,6 +1,6 @@
include "../src/karax" include "../karax/karax"
import "../src/karaxdsl" import "../karax/karaxdsl"
proc hasDom(n: Vnode) = proc hasDom(n: Vnode) =
if n.kind in {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}: if n.kind in {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}:

20
tests/nativehtmlgen.nim Normal file
View file

@ -0,0 +1,20 @@
## Simple test that shows Karax can also do client-side HTML rendering.
import "../karax" / [karaxdsl, vdom]
when defined(js):
{.error: "Use 'nim c' to compile this example".}
let tab = buildHtml(table):
tr:
td:
text "Cell A"
td:
text "Cell B"
tr:
td:
text "Cell C"
td:
text "Cell D"
echo tab

View file

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

View file

@ -14,5 +14,6 @@ proc main =
exec("nim js examples/mediaplayer/playerapp.nim") exec("nim js examples/mediaplayer/playerapp.nim")
exec("nim js examples/carousel/carousel.nim") exec("nim js examples/carousel/carousel.nim")
exec("nim js -d:nodejs -r tests/difftest.nim") exec("nim js -d:nodejs -r tests/difftest.nim")
exec("nim c tests/nativehtmlgen.nim")
main() main()