From 23fc33162609fb111024a1e0369dfd855283a92a Mon Sep 17 00:00:00 2001 From: Roman Vishnevskii Date: Thu, 27 Apr 2017 18:13:25 +0700 Subject: [PATCH 001/247] Add uibench --- uibench/index.html | 13 ++++ uibench/nim_uibench.nim | 130 +++++++++++++++++++++++++++++++++++++++ uibench/nim_uibench.nims | 1 + 3 files changed, 144 insertions(+) create mode 100644 uibench/index.html create mode 100644 uibench/nim_uibench.nim create mode 100644 uibench/nim_uibench.nims diff --git a/uibench/index.html b/uibench/index.html new file mode 100644 index 0000000..6b3e9c0 --- /dev/null +++ b/uibench/index.html @@ -0,0 +1,13 @@ + + + + + + UI Benchmark: React + + +
+ + + + \ No newline at end of file diff --git a/uibench/nim_uibench.nim b/uibench/nim_uibench.nim new file mode 100644 index 0000000..4ff4cb3 --- /dev/null +++ b/uibench/nim_uibench.nim @@ -0,0 +1,130 @@ +import dom, vdom, karax, karaxdsl, jdict, jstrutils, kajax + +type + HomeState = object + TableItemState = object + id: int + active: bool + props: seq[cstring] + TableState = object + items: seq[TableItemState] + AnimBoxState = object + id: int + time: int + AnimState = object + items: seq[AnimBoxState] + TreeNodeState = object + id: int + container: bool + children: seq[TreeNodeState] + TreeState = object + root: TreeNodeState + AppState = object + location: cstring + home: HomeState + table: TableState + anim: AnimState + tree: TreeState + +proc init*(a: cstring, b: cstring) {.importc: "uibench.init", nodecl.} +proc run*(a: proc(state: AppState), b: proc(samples: any)) {.importc: "uibench.run", nodecl.} + +init(cstring"Nim-karax", cstring"0.6.1") + +var container = document.getElementById("#App") +var appState: AppState + +proc createTableCell(id: cstring): VNode = + proc tableCellClick(ev: karax.Event; n: VNode) = + kout "Clicked" & id + # ev.stopPropogation() + result = buildHtml(td(className="TableCell", onclick=tableCellClick)): + text id + + +proc createTableRow(item: TableItemState): VNode = + var children = createTableCell("#" & &item.id) + for i in 0..len(item.props): + children.add(createTableCell(item.props[i])) + var dataId = cstring($item.id) + + proc createRow(): VNode = + var className = cstring"TableRow" + if item.active: + className = cstring"TableRow active" + result = buildHtml(tr(class=className, `data-id`=dataId)): + children + result = createRow() + + +proc tableCreateVNode(data: TableState): VNode = + var childrens: seq[VNode] = @[] + for i in 0..len(data.items): + childrens.add createTableRow(data.items[i]) + + proc createTable(): VNode = + result = buildHtml(table(class="Table")): + tbody: + for child in childrens: + child + + result = createTable() + +proc createAnimBox(item: AnimBoxState): VNode = + var time = item.time + var dataId: cstring = &item.id + var color: float = float(time mod 10) / 10 + var divStyles: cstring = "borderRadius: " & &(time mod 10) & "px; background: rgba(0,0,0," & cstring($color) & ")" + result = flatHtml(tdiv(className="AnimBox", `data-id`=dataId, style=divStyles)) + +proc animCreateVNode(data: AnimState): VNode = + var items = data.items + var children: seq[VNode] = @[] + for i in 0..len(children): + var item = items[i] + children.add createAnimBox(item) + result = buildHtml(tdiv(className="Anim")): + for child in children: + child + +proc createTreeLeaf(data: TreeNodeState): VNode = + result = buildHtml(li(className="TreeLeaf")): + text &data.id + +proc createTreeNode(data: TreeNodeState): VNode = + var children: seq[VNode] = @[] + for i in 0..len(data.children): + var n = data.children[i] + if n.container: + children.add(createTreeNode(n)) + else: + children.add(createTreeLeaf(n)) + +proc treeCreateVNode(data: TreeState): VNode = + result = buildHtml(tdiv(className="Tree")): + createTreeNode(data.root) + + +proc update(): VNode = + let location = appState.location + var children: VNode = nil + if location == cstring"table": + children = tableCreateVNode(appState.table) + elif location == cstring"anim": + children = animCreateVNode(appState.anim) + elif location == cstring"tree": + children = treeCreateVNode(appState.tree) + proc createMain(): VNode = + result = buildHtml(tdiv(class="Main")): children + result = createMain() + + +proc a(state: AppState) = + appState = state + +proc b(samples: RootRef) = + document.body.innerHTML = cstring"
" & toJson(samples) & cstring"
" + redraw() + +setRenderer update +run(a, b) \ No newline at end of file diff --git a/uibench/nim_uibench.nims b/uibench/nim_uibench.nims new file mode 100644 index 0000000..532b206 --- /dev/null +++ b/uibench/nim_uibench.nims @@ -0,0 +1 @@ +--path: "../src" From 1ad978804c1dced318a1d6e20988b7acc1b4fadb Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 27 Apr 2017 13:47:00 +0200 Subject: [PATCH 002/247] fixes --- uibench/nim_uibench.nim | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/uibench/nim_uibench.nim b/uibench/nim_uibench.nim index 4ff4cb3..44da28f 100644 --- a/uibench/nim_uibench.nim +++ b/uibench/nim_uibench.nim @@ -1,25 +1,25 @@ import dom, vdom, karax, karaxdsl, jdict, jstrutils, kajax type - HomeState = object - TableItemState = object + HomeState {.importc.} = ref object + TableItemState {.importc.} = ref object id: int active: bool props: seq[cstring] - TableState = object + TableState {.importc.} = ref object items: seq[TableItemState] - AnimBoxState = object + AnimBoxState {.importc.} = ref object id: int time: int - AnimState = object + AnimState {.importc.} = ref object items: seq[AnimBoxState] - TreeNodeState = object + TreeNodeState {.importc.} = ref object id: int container: bool children: seq[TreeNodeState] - TreeState = object + TreeState {.importc.} = ref object root: TreeNodeState - AppState = object + AppState {.importc.} = ref object location: cstring home: HomeState table: TableState @@ -27,7 +27,7 @@ type tree: TreeState proc init*(a: cstring, b: cstring) {.importc: "uibench.init", nodecl.} -proc run*(a: proc(state: AppState), b: proc(samples: any)) {.importc: "uibench.run", nodecl.} +proc run*(a: proc(state: AppState), b: proc(samples: RootRef)) {.importc: "uibench.run", nodecl.} init(cstring"Nim-karax", cstring"0.6.1") @@ -44,7 +44,7 @@ proc createTableCell(id: cstring): VNode = proc createTableRow(item: TableItemState): VNode = var children = createTableCell("#" & &item.id) - for i in 0..len(item.props): + for i in 0.." & toJson(samples) & cstring"" redraw() +kout cstring"updating" setRenderer update +kout cstring"running" run(a, b) \ No newline at end of file From 41df9b769f2268b9206a4efa9747e6c8fb7c563f Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 27 Apr 2017 14:05:11 +0200 Subject: [PATCH 003/247] more progress --- uibench/index.html | 3 +++ uibench/nim_uibench.nim | 32 +++++++++++++------------------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/uibench/index.html b/uibench/index.html index 6b3e9c0..bc77eb5 100644 --- a/uibench/index.html +++ b/uibench/index.html @@ -9,5 +9,8 @@
+ +
+ \ No newline at end of file diff --git a/uibench/nim_uibench.nim b/uibench/nim_uibench.nim index 44da28f..4c2b467 100644 --- a/uibench/nim_uibench.nim +++ b/uibench/nim_uibench.nim @@ -41,12 +41,11 @@ proc createTableCell(id: cstring): VNode = result = buildHtml(td(className="TableCell", onclick=tableCellClick)): text id - proc createTableRow(item: TableItemState): VNode = var children = createTableCell("#" & &item.id) for i in 0.." & toJson(samples) & cstring"" - redraw() + #document.body.innerHTML = cstring"
" & toJson(samples) & cstring"
" + #redraw() + discard kout cstring"updating" setRenderer update From 443b24de3e3ba3d7011fdc14c1e325f8bbd1fdf2 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 27 Apr 2017 14:16:34 +0200 Subject: [PATCH 004/247] more fixes --- src/karax.nim | 8 ++++++++ uibench/index.html | 5 +++-- uibench/nim_uibench.nim | 22 +++++++++++++--------- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index ed956d8..45056c5 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -256,6 +256,8 @@ proc redraw*() = else: dodraw() +proc redrawForce*() = dodraw() + proc init(ev: Event) = reqFrame(dodraw) @@ -263,6 +265,12 @@ proc setRenderer*(renderer: proc (): VNode) = dorender = renderer window.onload = init +proc setRendererOnly*(renderer: proc (): VNode) = + dorender = renderer + +proc setOnloadOnly*() = + window.onload = init + proc addEventHandler*(n: VNode; k: EventKind; action: EventHandler) = ## Implements the foundation of Karax's event management. ## Karax DSL transforms ``tag(onEvent = handler)`` to diff --git a/uibench/index.html b/uibench/index.html index bc77eb5..4d0e517 100644 --- a/uibench/index.html +++ b/uibench/index.html @@ -7,10 +7,11 @@
- -
+ + + \ No newline at end of file diff --git a/uibench/nim_uibench.nim b/uibench/nim_uibench.nim index 4c2b467..b8e453e 100644 --- a/uibench/nim_uibench.nim +++ b/uibench/nim_uibench.nim @@ -26,11 +26,6 @@ type anim: AnimState tree: TreeState -proc init*(a: cstring, b: cstring) {.importc: "uibench.init", nodecl.} -proc run*(a: proc(state: AppState), b: proc(samples: RootRef)) {.importc: "uibench.run", nodecl.} - -init(cstring"Nim-karax", cstring"0.6.1") - var container = document.getElementById("#App") var appState: AppState @@ -98,7 +93,9 @@ proc treeCreateVNode(data: TreeState): VNode = proc update(): VNode = - if appState == nil: return newVNode(VNodeKind.tdiv) + if appState == nil: + kout cstring"stupid fuck" + return newVNode(VNodeKind.tdiv) let location = appState.location var children: VNode = nil if location == cstring"table": @@ -113,15 +110,22 @@ proc update(): VNode = children proc a(state: AppState) = + kout cstring"setting state here" appState = state - redraw() + redrawForce() proc b(samples: RootRef) = + kout cstring"end called" #document.body.innerHTML = cstring"
" & toJson(samples) & cstring"
" #redraw() discard -kout cstring"updating" -setRenderer update +proc init*(a: cstring, b: cstring) {.importc: "uibench.init", nodecl.} +proc run*(a: proc(state: AppState), b: proc(samples: RootRef)) {.importc: "uibench.run", nodecl.} + +init(cstring"Nim-karax", cstring"0.6.1") + +setRendererOnly update kout cstring"running" + run(a, b) \ No newline at end of file From a30432228689d0d8eb739d84c968f442f0d15c00 Mon Sep 17 00:00:00 2001 From: Roman Vishnevskii Date: Thu, 27 Apr 2017 20:49:54 +0700 Subject: [PATCH 005/247] Uibench fix td --- uibench/nim_uibench.nim | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/uibench/nim_uibench.nim b/uibench/nim_uibench.nim index b8e453e..ad3378d 100644 --- a/uibench/nim_uibench.nim +++ b/uibench/nim_uibench.nim @@ -30,34 +30,43 @@ var container = document.getElementById("#App") var appState: AppState proc createTableCell(id: cstring): VNode = + kout cstring"createTableCell" proc tableCellClick(ev: karax.Event; n: VNode) = + kout cstring"tableCellClick" kout "Clicked" & id # ev.stopPropogation() result = buildHtml(td(className="TableCell", onclick=tableCellClick)): text id proc createTableRow(item: TableItemState): VNode = - var children = createTableCell("#" & &item.id) + kout cstring"createTableRow" + var children: seq[VNode] = @[] + children.add createTableCell("#" & &item.id) for i in 0.. Date: Thu, 27 Apr 2017 20:57:33 +0700 Subject: [PATCH 006/247] Change className to class --- uibench/nim_uibench.nim | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/uibench/nim_uibench.nim b/uibench/nim_uibench.nim index ad3378d..5e1d3af 100644 --- a/uibench/nim_uibench.nim +++ b/uibench/nim_uibench.nim @@ -35,7 +35,7 @@ proc createTableCell(id: cstring): VNode = kout cstring"tableCellClick" kout "Clicked" & id # ev.stopPropogation() - result = buildHtml(td(className="TableCell", onclick=tableCellClick)): + result = buildHtml(td(class="TableCell", onclick=tableCellClick)): text id proc createTableRow(item: TableItemState): VNode = @@ -71,7 +71,7 @@ proc createAnimBox(item: AnimBoxState): VNode = var dataId: cstring = &item.id var color: float = float(time mod 10) / 10 var divStyles: cstring = "borderRadius: " & &(time mod 10) & "px; background: rgba(0,0,0," & cstring($color) & ")" - result = flatHtml(tdiv(className="AnimBox", `data-id`=dataId, style=divStyles)) + result = flatHtml(tdiv(class="AnimBox", `data-id`=dataId, style=divStyles)) proc animCreateVNode(data: AnimState): VNode = kout cstring"animCreateVNode" @@ -80,13 +80,13 @@ proc animCreateVNode(data: AnimState): VNode = for i in 0.. Date: Thu, 27 Apr 2017 21:58:47 +0700 Subject: [PATCH 007/247] Uibench fix border-radius --- uibench/nim_uibench.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uibench/nim_uibench.nim b/uibench/nim_uibench.nim index 5e1d3af..e21eca8 100644 --- a/uibench/nim_uibench.nim +++ b/uibench/nim_uibench.nim @@ -70,14 +70,14 @@ proc createAnimBox(item: AnimBoxState): VNode = var time = item.time var dataId: cstring = &item.id var color: float = float(time mod 10) / 10 - var divStyles: cstring = "borderRadius: " & &(time mod 10) & "px; background: rgba(0,0,0," & cstring($color) & ")" + var divStyles: cstring = "border-radius: " & &(time mod 10) & "px; background: rgba(0,0,0," & cstring($color) & ")" result = flatHtml(tdiv(class="AnimBox", `data-id`=dataId, style=divStyles)) proc animCreateVNode(data: AnimState): VNode = kout cstring"animCreateVNode" var items = data.items var children: seq[VNode] = @[] - for i in 0.. Date: Thu, 27 Apr 2017 22:22:18 +0700 Subject: [PATCH 008/247] Uibench final fix --- uibench/index.html | 3 +-- uibench/nim_uibench.nim | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/uibench/index.html b/uibench/index.html index 4d0e517..5b2cb4c 100644 --- a/uibench/index.html +++ b/uibench/index.html @@ -6,7 +6,6 @@ UI Benchmark: React -
@@ -14,4 +13,4 @@ - \ No newline at end of file + diff --git a/uibench/nim_uibench.nim b/uibench/nim_uibench.nim index e21eca8..f6c214b 100644 --- a/uibench/nim_uibench.nim +++ b/uibench/nim_uibench.nim @@ -98,6 +98,10 @@ proc createTreeNode(data: TreeNodeState): VNode = children.add(createTreeNode(n)) else: children.add(createTreeLeaf(n)) + result = buildHtml(ul(class="TreeNode")): + for child in children: + child + proc treeCreateVNode(data: TreeState): VNode = kout cstring"treeCreateVNode" From 393c336f901f179147c78e7d8bbc51eda25567b1 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 27 Apr 2017 17:26:01 +0200 Subject: [PATCH 009/247] simplifications --- uibench/nim_uibench.nim | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/uibench/nim_uibench.nim b/uibench/nim_uibench.nim index 5e1d3af..e88cef8 100644 --- a/uibench/nim_uibench.nim +++ b/uibench/nim_uibench.nim @@ -26,7 +26,6 @@ type anim: AnimState tree: TreeState -var container = document.getElementById("#App") var appState: AppState proc createTableCell(id: cstring): VNode = @@ -44,18 +43,13 @@ proc createTableRow(item: TableItemState): VNode = children.add createTableCell("#" & &item.id) for i in 0.. Date: Thu, 27 Apr 2017 17:39:54 +0200 Subject: [PATCH 010/247] simplifications --- uibench/nim_uibench.nim | 56 +++++++++++------------------------------ 1 file changed, 14 insertions(+), 42 deletions(-) diff --git a/uibench/nim_uibench.nim b/uibench/nim_uibench.nim index e1c644b..a9a7d96 100644 --- a/uibench/nim_uibench.nim +++ b/uibench/nim_uibench.nim @@ -29,79 +29,57 @@ type var appState: AppState proc createTableCell(id: cstring): VNode = - kout cstring"createTableCell" proc tableCellClick(ev: karax.Event; n: VNode) = - kout cstring"tableCellClick" kout "Clicked" & id - # ev.stopPropogation() result = buildHtml(td(class="TableCell", onclick=tableCellClick)): text id proc createTableRow(item: TableItemState): VNode = - kout cstring"createTableRow" - var children: seq[VNode] = @[] - children.add createTableCell("#" & &item.id) - for i in 0.." & toJson(samples) & cstring"" - #redraw() - discard + document.body.innerHTML = cstring"
" & toJson(samples) & cstring"
" proc init*(a: cstring, b: cstring) {.importc: "uibench.init", nodecl.} proc run*(a: proc(state: AppState), b: proc(samples: RootRef)) {.importc: "uibench.run", nodecl.} @@ -131,6 +105,4 @@ proc run*(a: proc(state: AppState), b: proc(samples: RootRef)) {.importc: "uiben init(cstring"Nim-karax", cstring"0.6.1") setRendererOnly update -kout cstring"running" - run(a, b) \ No newline at end of file From 8bdaab3f821ba902cf4d4d874714303bad1bc851 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 27 Apr 2017 17:57:04 +0200 Subject: [PATCH 011/247] uibenchmark works --- uibench/nim_uibench.nim | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/uibench/nim_uibench.nim b/uibench/nim_uibench.nim index a9a7d96..2983686 100644 --- a/uibench/nim_uibench.nim +++ b/uibench/nim_uibench.nim @@ -96,8 +96,10 @@ proc a(state: AppState) = appState = state redrawForce() +proc myToJson[T](x: T): cstring {.importcpp: "JSON.stringify(#, null, ' ')".} + proc b(samples: RootRef) = - document.body.innerHTML = cstring"
" & toJson(samples) & cstring"
" + document.body.innerHTML = cstring"
" & myToJson(samples) & cstring"
" proc init*(a: cstring, b: cstring) {.importc: "uibench.init", nodecl.} proc run*(a: proc(state: AppState), b: proc(samples: RootRef)) {.importc: "uibench.run", nodecl.} From 91679f8ae607a1d6a721a0d68e6fcda5df5a8998 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 27 Apr 2017 18:23:29 +0200 Subject: [PATCH 012/247] made it shorter again --- uibench/nim_uibench.nim | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/uibench/nim_uibench.nim b/uibench/nim_uibench.nim index 2983686..3d1f078 100644 --- a/uibench/nim_uibench.nim +++ b/uibench/nim_uibench.nim @@ -35,9 +35,7 @@ proc createTableCell(id: cstring): VNode = text id proc createTableRow(item: TableItemState): VNode = - var className = cstring"TableRow" - if item.active: - className = cstring"TableRow active" + let className = if item.active: cstring"TableRow active" else: cstring"TableRow" result = buildHtml(tr(class=className, `data-id` = &item.id)): createTableCell("#" & &item.id) for i in 0.. Date: Mon, 1 May 2017 09:58:10 +0200 Subject: [PATCH 013/247] optimized VDOM implementation for uibench --- src/vdom.nim | 64 ++++++++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/src/vdom.nim b/src/vdom.nim index 25f6c12..59fa1d6 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -106,8 +106,9 @@ type # even index: key, odd index: value; done this way for memory efficiency: attrs: seq[cstring] events*: seq[(EventKind, EventHandler)] - hash*: Hash - validHash*: bool + when false: + hash*: Hash + validHash*: bool dom*: Node ## the attached real DOM node. Can be 'nil' if the virtual node ## is not part of the virtual DOM anymore. @@ -146,7 +147,11 @@ proc eq*(a, b: VNode): bool = proc setAttr*(n: VNode; key: cstring; val: cstring = "") = if n.attrs.isNil: - n.attrs = @[key, val] + when true: + let x = @[key, val] + shallowCopy n.attrs, x + else: + n.attrs = @[key, val] else: for i in countup(0, n.attrs.len-2, 2): if n.attrs[i] == key: @@ -216,32 +221,33 @@ proc toString*(n: VNode; result: var string; indent: int) = for i in 1..indent: result.add ' ' result.add "\L" -proc calcHash*(n: VNode) = - if n.validHash: return - n.validHash = true - var h: Hash = ord n.kind - if n.id != nil: - h &= "id" - h &= n.id - if n.class != nil: - h &= "class" - h &= n.class - if n.key >= 0: - h &= "k" - h &= n.key - for k, v in attrs(n): - h &= " " - h &= k - h &= "=" - h &= v - if n.kind == VNodeKind.text or n.text != nil: - h &= "t" - h &= n.text - else: - for child in items(n): - calcHash(child) - h &= child.hash - n.hash = h +when false: + proc calcHash*(n: VNode) = + if n.validHash: return + n.validHash = true + var h: Hash = ord n.kind + if n.id != nil: + h &= "id" + h &= n.id + if n.class != nil: + h &= "class" + h &= n.class + if n.key >= 0: + h &= "k" + h &= n.key + for k, v in attrs(n): + h &= " " + h &= k + h &= "=" + h &= v + if n.kind == VNodeKind.text or n.text != nil: + h &= "t" + h &= n.text + else: + for child in items(n): + calcHash(child) + h &= child.hash + n.hash = h proc `$`*(n: VNode): cstring = var res = "" From 4c582d7c06ffc10b72c65702fb188e603a8ded9e Mon Sep 17 00:00:00 2001 From: Roman Vishnevskii Date: Tue, 2 May 2017 19:46:20 +0700 Subject: [PATCH 014/247] Avoid using node.childNodes --- src/karax.nim | 75 ++++++++++++++++++++++++++++++++------------------- src/vdom.nim | 1 + 2 files changed, 48 insertions(+), 28 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 45056c5..1ef0a9b 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -164,7 +164,13 @@ proc updateDirtyElements(parent, current: Node, newNode: VNode) = # leave early if we know there cannot be anything left to do: #if dirtyCount <= 0: return +proc setDomField(a, b: VNode) = + a.dom = b.dom + for i in 0..= commonPrefix and newPos >= commonPrefix and - equalsTree(newNode[newPos], oldNode[oldPos]): - dec oldPos - dec newPos + while rightOld > left and rightNew > left and + equalsTree(newNode[rightNew], oldNode[rightOld]): + #newNode[rightNew].dom = oldNode[rightOld].dom + setDomField(newNode[rightNew], oldNode[rightOld]) + dec rightOld + dec rightNew + + minRight = min(rightOld, rightNew) + if left <= minRight: + updateElement(current, oldNode[left].dom, newNode[left], oldNode[left]) + inc left - var pos = min(oldPos, newPos) + 1 - for i in commonPrefix..pos-1: - updateElement(current, current.childNodes[i], newNode[i], oldNode[i]) + if left < minRight: + updateElement(current, oldNode[rightOld].dom, newNode[rightNew], oldNode[rightOld]) + dec rightNew + dec rightOld + + var nextChildPos = rightOld + 1 + while left <= rightNew: + if nextChildPos == oldLength: + current.appendChild(vnodeToDom(newNode[left])) + else: + current.insertBefore(vnodeToDom(newNode[left]), oldNode[rightOld + 1].dom) + # added new Node, so old state of VDOM have one more Node + inc oldLength + inc left + inc nextChildPos - var nextChildPos = oldPos + 1 - while pos <= newPos: - if nextChildPos == oldLength: - current.appendChild(vnodeToDom(newNode[pos])) - else: - current.insertBefore(vnodeToDom(newNode[pos]), current.childNodes[nextChildPos]) - # added new Node, so old state of VDOM have one more Node - inc oldLength - inc pos - inc nextChildPos - - for i in pos..oldPos: - detach(oldNode[i]) - current.removeChild(current.childNodes[pos]) + for i in left..rightOld: + current.removeChild(oldNode[i].dom) + detach(oldNode[i]) when false: var drawTimeout: Timeout diff --git a/src/vdom.nim b/src/vdom.nim index 59fa1d6..48574de 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -166,6 +166,7 @@ proc getAttr*(n: VNode; key: cstring): cstring = proc len*(x: VNode): int = x.kids.len proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx] +proc `[]=`*(x: VNode; idx: int, y: VNode) = x.kids[idx] = y proc add*(parent, kid: VNode) = parent.kids.add kid proc newVNode*(kind: VNodeKind): VNode = VNode(kind: kind, key: -1) From cd78f5085f0619b0cbbeb6c1f7f67a40d855eb85 Mon Sep 17 00:00:00 2001 From: Roman Vishnevskii Date: Tue, 2 May 2017 19:50:35 +0700 Subject: [PATCH 015/247] Simplify updateElement --- src/karax.nim | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 1ef0a9b..23dc324 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -222,17 +222,16 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) = updateElement(current, oldNode[rightOld].dom, newNode[rightNew], oldNode[rightOld]) dec rightNew dec rightOld - - var nextChildPos = rightOld + 1 + + var posNextElem = rightOld + 1 + var isPushBack = posNextElem == oldLength while left <= rightNew: - if nextChildPos == oldLength: + if isPushBack: current.appendChild(vnodeToDom(newNode[left])) else: - current.insertBefore(vnodeToDom(newNode[left]), oldNode[rightOld + 1].dom) + current.insertBefore(vnodeToDom(newNode[left]), oldNode[posNextElem].dom) # added new Node, so old state of VDOM have one more Node - inc oldLength inc left - inc nextChildPos for i in left..rightOld: current.removeChild(oldNode[i].dom) From e63a7dbb21490464fd59a0ba77bb02e9af098de3 Mon Sep 17 00:00:00 2001 From: Roman Vishnevskii Date: Wed, 3 May 2017 14:48:36 +0700 Subject: [PATCH 016/247] Delete useless comment --- src/karax.nim | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 23dc324..85a126a 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -164,11 +164,6 @@ proc updateDirtyElements(parent, current: Node, newNode: VNode) = # leave early if we know there cannot be anything left to do: #if dirtyCount <= 0: return -proc setDomField(a, b: VNode) = - a.dom = b.dom - for i in 0.. left and rightNew > left and equalsTree(newNode[rightNew], oldNode[rightOld]): - #newNode[rightNew].dom = oldNode[rightOld].dom - setDomField(newNode[rightNew], oldNode[rightOld]) + newNode[rightNew] = oldNode[rightOld] dec rightOld dec rightNew @@ -230,7 +223,6 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) = current.appendChild(vnodeToDom(newNode[left])) else: current.insertBefore(vnodeToDom(newNode[left]), oldNode[posNextElem].dom) - # added new Node, so old state of VDOM have one more Node inc left for i in left..rightOld: From 78a47158a1da96f5ca652bc72cf61f785a8c0cf2 Mon Sep 17 00:00:00 2001 From: Roman Vishnevskii Date: Thu, 4 May 2017 14:35:44 +0700 Subject: [PATCH 017/247] Introduce track field --- src/karaxdsl.nim | 2 +- src/vdom.nim | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index 5ba525a..5553d8f 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -4,7 +4,7 @@ from strutils import startsWith, toLowerAscii const StmtContext = ["kout", "inc", "echo", "dec", "!"] - SpecialAttrs = ["id", "class", "value", "key"] + SpecialAttrs = ["id", "class", "value", "key", "track"] proc getName(n: NimNode): string = case n.kind diff --git a/src/vdom.nim b/src/vdom.nim index 48574de..a80fc10 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -102,6 +102,7 @@ type kind*: VNodeKind key*: VKey id*, class*, text*: cstring + track*: int kids: seq[VNode] # even index: key, odd index: value; done this way for memory efficiency: attrs: seq[cstring] @@ -166,7 +167,7 @@ proc getAttr*(n: VNode; key: cstring): cstring = proc len*(x: VNode): int = x.kids.len proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx] -proc `[]=`*(x: VNode; idx: int, y: VNode) = x.kids[idx] = y +proc `[]=`*(x: VNode; idx: int; y: VNode) = x.kids[idx] = y proc add*(parent, kid: VNode) = parent.kids.add kid proc newVNode*(kind: VNodeKind): VNode = VNode(kind: kind, key: -1) From 1b56477e5989b5026510059a815d8023af09ffe9 Mon Sep 17 00:00:00 2001 From: Roman Vishnevskii Date: Wed, 10 May 2017 14:38:16 +0700 Subject: [PATCH 018/247] Cross comparing (debug) --- src/karax.nim | 193 ++++++++++++++++++++++++++++++++-------- tests/diffDomTests.html | 1 - tests/diffDomTests.nim | 38 ++++---- 3 files changed, 173 insertions(+), 59 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 85a126a..ed4ab39 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -130,9 +130,6 @@ proc equalsShallow(a, b: VNode): bool = if a.text != b.text: return false elif a.kind == VNodeKind.vthunk or a.kind == VNodeKind.dthunk: if a.text != b.text: return false - if a.len != b.len: return false - for i in 0..= left and rightNew >= left and equalsShallow(newNode[rightNew], oldNode[rightOld]): + updateElement(current, oldNode[rightOld].dom, newNode[rightNew], oldNode[rightOld]) + dec rightOld + dec rightNew + + var leftOld = left + var leftNew = left - while left <= min(rightNew, rightOld): - var minRight = min(rightOld, rightNew) - - while left <= minRight and - equalsTree(newNode[left], oldNode[left]): - newNode[left] = oldNode[left] - inc left + + var flag = false + #if rightOld >= leftOld and rightNew >= leftNew and equalsShallow(oldNode[leftOld], newNode[rightNew]): + kout cstring("current") + kout printChildren(current) + kout cstring("oldNode") + kout printChildren(oldNode) + kout cstring("newNode") + kout printChildren(newNode) + flag = true + + var step = 1 + while rightOld >= leftOld and rightNew >= leftNew and equalsShallow(oldNode[leftOld], newNode[rightNew]): + kout cstring($oldNode[leftOld].id & " " & $newNode[rightNew].id) + kout cstring("pos: " & $leftOld & " " & $rightNew) - while rightOld > left and rightNew > left and - equalsTree(newNode[rightNew], oldNode[rightOld]): - newNode[rightNew] = oldNode[rightOld] - dec rightOld - dec rightNew - - minRight = min(rightOld, rightNew) - if left <= minRight: - updateElement(current, oldNode[left].dom, newNode[left], oldNode[left]) - inc left + var nextNode: Node = nil + if rightNew + 1 < newLength: + nextNode = newNode[rightNew + 1].dom - if left < minRight: - updateElement(current, oldNode[rightOld].dom, newNode[rightNew], oldNode[rightOld]) - dec rightNew - dec rightOld - - var posNextElem = rightOld + 1 - var isPushBack = posNextElem == oldLength - while left <= rightNew: - if isPushBack: - current.appendChild(vnodeToDom(newNode[left])) - else: - current.insertBefore(vnodeToDom(newNode[left]), oldNode[posNextElem].dom) - inc left + updateElement(current, oldNode[leftOld].dom, newNode[rightNew], oldNode[leftOld]) + if nextNode == nil: + current.appendChild(oldNode[leftOld].dom) + kout cstring("append") + else: + kout cstring("insertBefore") + current.insertBefore(oldNode[leftOld].dom, nextNode) + #kout cstring("step = " & $step & "; leftOld = " & $leftOld & "; rightNew = " & $rightNew) + kout cstring($oldNode[leftOld]) + inc step + inc leftOld + dec rightNew + + if flag: + kout cstring("after") + kout printChildren(current) + - for i in left..rightOld: - current.removeChild(oldNode[i].dom) - detach(oldNode[i]) + while rightOld >= leftOld and rightNew >= leftNew: + updateElement(current, current[leftOld], newNode[leftNew], oldNode[leftOld]) + inc leftNew + inc leftOld + + var isPushBack = (rightNew + 1 == newLength) + var nextNode: Node = nil + if not isPushBack: + nextNode = newNode[rightNew + 1].dom + while leftNew <= rightNew: + var node = vnodeToDom(newNode[leftNew])ды + if isPushBack: + current.appendChild(node) + else: + current.insertBefore(node, nextNode) + inc leftNew + + for i in leftOld..rightOld: + current.removeChild(oldNode[i].dom) + detach(oldNode[i]) + + kout cstring("----------------") + kout cstring("before") + kout before + kout cstring("finish") + kout printChildren(current) + kout cstring("----------------") + kout cstring("----------------") + + + # var leftNew = left + # var leftOld = left + # while rightOld >= leftOld and rightNew >= leftNew and equalsShallow(oldNode[leftOld], newNode[rightNew]): + # var nextNode: Node = nil + # if rightNew + 1 < newLength: + # nextNode = newNode[rightNew + 1].dom + # if nextNode == nil: + # current.appendChild(oldNode[leftOld].dom) + # else: + # current.insertBefore(oldNode[leftOld].dom, nextNode) + # inc leftOld + # dec rightNew + + # var lastTarget: Node = nil + # if leftOld <= rightOld: + # lastTarget = oldNode[leftOld].dom + # while rightOld >= leftOld and rightNew >= leftNew and equalsShallow(oldNode[rightOld], newNode[leftNew]): + # current.insertBefore(oldNode[rightOld].dom, lastTarget) + # lastTarget = oldNode[rightOld].dom + # inc leftNew + # dec rightOld + + # if rightOld < leftOld: + # var isPushBack = leftOld < oldLength + # for i in leftNew..rightNew: + # if isPushBack: + # current.appendChild(vnodeToDom(newNode[i])) + # else: + # current.insertBefore(vnodeToDom(newNode[i]), oldNode[leftOld].dom) + # elif rightNew < leftNew: + # for i in leftOld..rightOld: + # current.removeChild(oldNode[i].dom) + # detach(oldNode[i]) + # else: + # while rightOld >= leftOld and rightNew >= leftNew: + # updateElement(current, oldNode[leftOld].dom, newNode[leftNew], oldNode[leftOld]) + # inc leftNew + # inc leftOld + # if rightOld < leftOld: + # while rightNew >= leftNew: + # current.appendChild(vnodeToDom(newNode[leftNew])) + # inc leftNew + # elif rightNew < leftNew: + # while rightOld >= leftOld: + # current.removeChild(oldNode[leftOld].dom) + # detach(oldNode[leftOld]) + # inc leftOld when false: var drawTimeout: Timeout diff --git a/tests/diffDomTests.html b/tests/diffDomTests.html index 5ecf130..cb6ab28 100644 --- a/tests/diffDomTests.html +++ b/tests/diffDomTests.html @@ -3,7 +3,6 @@ Diff dom tests - diff --git a/tests/diffDomTests.nim b/tests/diffDomTests.nim index 7fdcf85..403980a 100644 --- a/tests/diffDomTests.nim +++ b/tests/diffDomTests.nim @@ -41,7 +41,7 @@ proc test1() = proc check2() = let result = checkOrder(@[8, 0, 1, 2, 3, 4, 7, 5]) if result: - results.add cstring"test1 - OK" + results.add cstring"test2 - OK" else: results.add cstring"test2 - FAIL" @@ -134,7 +134,7 @@ proc createDom(): VNode = result = buildHtml(tdiv()): ul(id="ul"): for e in entries: - createEntry(parseInt(e)) + createEntry(jstrutils.parseInt(e)) for r in results: tdiv: text r @@ -147,20 +147,20 @@ proc onload() = var dtTest = 500 var t = dtReset - timeout = setTimeout(test1, t) - t += dtTest - timeout = setTimeout(reset, t) - t += dtReset + # timeout = setTimeout(test1, t) + # t += dtTest + # timeout = setTimeout(reset, t) + # t += dtReset - timeout = setTimeout(test2, t) - t += dtTest - timeout = setTimeout(reset, t) - t += dtReset + # timeout = setTimeout(test2, t) + # t += dtTest + # timeout = setTimeout(reset, t) + # t += dtReset - timeout = setTimeout(test3, t) - t += dtTest - timeout = setTimeout(reset, t) - t += dtReset + # timeout = setTimeout(test3, t) + # t += dtTest + # timeout = setTimeout(reset, t) + # t += dtReset timeout = setTimeout(test4, t) t += dtTest @@ -172,12 +172,12 @@ proc onload() = timeout = setTimeout(reset, t) t += dtReset - timeout = setTimeout(test6, t) - t += dtTest - timeout = setTimeout(reset, t) - t += dtReset + # timeout = setTimeout(test6, t) + # t += dtTest + # timeout = setTimeout(reset, t) + # t += dtReset - timeout = setTimeout(test7, t) + # timeout = setTimeout(test7, t) onload() setRenderer createDom From ae448f947f782881647fd0bd31ef416776857d58 Mon Sep 17 00:00:00 2001 From: Roman Vishnevskii Date: Wed, 10 May 2017 17:38:51 +0700 Subject: [PATCH 019/247] Add LIS opt --- src/karax.nim | 326 ++++++++++++++++++++++++----------------- tests/diffDomTests.nim | 34 ++--- 2 files changed, 208 insertions(+), 152 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index ed4ab39..e661b78 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -1,6 +1,6 @@ ## Karax -- Single page applications for Nim. -import dom, vdom, jstrutils, components, jdict +import dom, vdom, jstrutils, components, jdict, tables export dom.Event @@ -123,28 +123,20 @@ proc replaceById(id: cstring; newTree: Node) = #newTree.id = id proc equalsShallow(a, b: VNode): bool = - if a.kind != b.kind: return false - if a.id != b.id: return false - if a.key != b.key: return false - if a.kind == VNodeKind.text: - if a.text != b.text: return false - elif a.kind == VNodeKind.vthunk or a.kind == VNodeKind.dthunk: - if a.text != b.text: return false - if not sameAttrs(a, b): return false - if a.class != b.class: return false - # XXX test event listeners here? - return true - -proc equalsTree(a, b: VNode): bool = - when false: - # hashing is too fragile now with component support: - if not a.validHash: - a.calcHash() - if not b.validHash: - b.calcHash() - return a.hash == b.hash + if a.key == -1 and b.key == -1: + return eq(a, b) else: - result = eq(a, b) + if a.kind != b.kind: return false + if a.id != b.id: return false + if a.key != b.key: return false + if a.kind == VNodeKind.text: + if a.text != b.text: return false + elif a.kind == VNodeKind.vthunk or a.kind == VNodeKind.dthunk: + if a.text != b.text: return false + if not sameAttrs(a, b): return false + if a.class != b.class: return false + # XXX test event listeners here? + return true proc updateDirtyElements(parent, current: Node, newNode: VNode) = if newNode.key >= 0 and isDirty(newNode.key): @@ -162,26 +154,68 @@ proc updateDirtyElements(parent, current: Node, newNode: VNode) = #if dirtyCount <= 0: return proc printChildren(parent: Node): cstring = - if parent != nil and parent.hasChildNodes: - var it = parent.firstChild - result = "" - while it != nil: - if it.id == nil: - result.add(" nil") - else: - result.add(" " & $it.id) - it = it.nextSibling + discard + # if parent != nil and parent.hasChildNodes: + # var it = parent.firstChild + # result = "" + # while it != nil: + # if it.id == nil: + # result.add(" nil") + # else: + # result.add(" " & $it.id) + # it = it.nextSibling proc printChildren(parent: VNode): cstring = - if parent != nil: - result = "" - for i in 0..parent.len-1: - if parent[i] == nil or parent[i].id == nil: - kout cstring("nil") - else: - result.add(" " & $(parent[i].id)) + discard + # if parent != nil: + # result = "" + # for i in 0..parent.len-1: + # if parent[i] == nil or parent[i].id == nil: + # result.add(" nil") + # else: + # result.add(" " & $(parent[i].id)) -proc updateElement(parent, current: Node, newNode, oldNode: VNode) = +proc print(s: cstring, ident: int) = + discard + # var result = "" + # for i in 0..ident: + # result.add " " + # result.add(s) + # kout cstring(result) + +proc longestIncreasingSubsequence(a: seq[int]): seq[int] = + result.add 0 + var parent = newSeq[int](len(a)) + for i in 0.. 0: + parent[i] = result[left - 1] + result[left] = i + + var pos = len(result) + var v = result[pos - 1] + while pos > 0: + result[pos] = v + v = parent[v] + dec pos + +proc updateElement(parent, current: Node, newNode, oldNode: VNode, ident: int = 0) = newNode.dom = oldNode.dom if not equalsShallow(newNode, oldNode): detach(oldNode) @@ -207,21 +241,21 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) = current.removeChild(current.lastChild) else: #kout cstring("start") - kout cstring("----------------") - kout cstring("----------------") + print("----------------", ident) + print("----------------", ident) var before = printChildren(current) # maximal common prefix var left = 0 while left < minLength and equalsShallow(newNode[left], oldNode[left]): - updateElement(current, oldNode[left].dom, newNode[left], oldNode[left]) + updateElement(current, oldNode[left].dom, newNode[left], oldNode[left], ident + 1) inc left # maximal common suffix var rightOld = oldLength - 1 var rightNew = newLength - 1 while rightOld >= left and rightNew >= left and equalsShallow(newNode[rightNew], oldNode[rightOld]): - updateElement(current, oldNode[rightOld].dom, newNode[rightNew], oldNode[rightOld]) + updateElement(current, oldNode[rightOld].dom, newNode[rightNew], oldNode[rightOld], ident + 1) dec rightOld dec rightNew @@ -231,118 +265,140 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) = var flag = false #if rightOld >= leftOld and rightNew >= leftNew and equalsShallow(oldNode[leftOld], newNode[rightNew]): - kout cstring("current") - kout printChildren(current) - kout cstring("oldNode") - kout printChildren(oldNode) - kout cstring("newNode") - kout printChildren(newNode) + print("current", ident) + print(printChildren(current), ident) + print("oldNode", ident) + print(printChildren(oldNode), ident) + print("newNode", ident) + print(printChildren(newNode), ident) flag = true - var step = 1 + # cross comparing while rightOld >= leftOld and rightNew >= leftNew and equalsShallow(oldNode[leftOld], newNode[rightNew]): - kout cstring($oldNode[leftOld].id & " " & $newNode[rightNew].id) - kout cstring("pos: " & $leftOld & " " & $rightNew) + print($oldNode[leftOld].id & " " & $newNode[rightNew].id, ident) + print("pos: " & $leftOld & " " & $rightNew, ident) var nextNode: Node = nil if rightNew + 1 < newLength: nextNode = newNode[rightNew + 1].dom - - updateElement(current, oldNode[leftOld].dom, newNode[rightNew], oldNode[leftOld]) + print("update", ident) + updateElement(current, oldNode[leftOld].dom, newNode[rightNew], oldNode[leftOld], ident + 1) + print("update", ident) if nextNode == nil: current.appendChild(oldNode[leftOld].dom) - kout cstring("append") + print("append", ident) else: - kout cstring("insertBefore") + print("insertBefore", ident) current.insertBefore(oldNode[leftOld].dom, nextNode) - #kout cstring("step = " & $step & "; leftOld = " & $leftOld & "; rightNew = " & $rightNew) - kout cstring($oldNode[leftOld]) - inc step + print($oldNode[leftOld], ident) inc leftOld dec rightNew - + + while rightOld >= leftOld and rightNew >= leftNew and equalsShallow(oldNode[rightOld], newNode[leftNew]): + var nextNode: Node = oldNode[leftOld].dom + updateElement(current, oldNode[rightOld].dom, newNode[leftNew], oldNode[rightOld], ident + 1) + current.insertBefore(oldNode[rightOld].dom, nextNode) + inc leftNew + dec rightOld + if flag: - kout cstring("after") - kout printChildren(current) - + print("after", ident) + print(printChildren(current), ident) - while rightOld >= leftOld and rightNew >= leftNew: - updateElement(current, current[leftOld], newNode[leftNew], oldNode[leftOld]) - inc leftNew - inc leftOld - - var isPushBack = (rightNew + 1 == newLength) - var nextNode: Node = nil - if not isPushBack: - nextNode = newNode[rightNew + 1].dom - while leftNew <= rightNew: - var node = vnodeToDom(newNode[leftNew])ды - if isPushBack: - current.appendChild(node) - else: - current.insertBefore(node, nextNode) - inc leftNew + var isKeyed = true + for i in leftNew..rightNew: + if newNode[i].key == -1: + isKeyed = false + break for i in leftOld..rightOld: - current.removeChild(oldNode[i].dom) - detach(oldNode[i]) + if oldNode[i].key == -1: + isKeyed = false + if not isKeyed: + break - kout cstring("----------------") - kout cstring("before") - kout before - kout cstring("finish") - kout printChildren(current) - kout cstring("----------------") - kout cstring("----------------") - + if isKeyed: + if rightNew > leftNew: + # remove redundant old nodes + for i in leftOld..rightOld: + current.removeChild(oldNode[i].dom) + detach(oldNode[i]) + else: + # permute elements using LIS + var positionByKey = newTable[VKey, int]() + var positions = newSeq[int]() + for i in leftOld..rightOld: + positionByKey[oldNode[i].key] = i + for i in leftNew..rightNew: + if positionByKey.hasKey(newNode[i].key): + positions.add positionByKey[newNode[i].key] + + var lis = longestIncreasingSubsequence(positions) + var lisPos = 0 + var isNotRedundant = newSeq[bool](rightOld - leftOld + 1) + for i in leftNew..rightNew: + if lisPos < len(lis): + var index = lis[lisPos] + if oldNode[index].key == newNode[i].key: + isNotRedundant[index - leftOld] = true + updateElement(current, oldNode[index].dom, newNode[i], oldNode[index], ident + 1) + inc lisPos + else: + if positionByKey.hasKey(newNode[i].key): + var oldPos = positionByKey[newNode[i].key] + isNotRedundant[oldPos - leftOld] = true + current.insertBefore(oldNode[oldPos].dom, oldNode[index].dom) + else: + current.insertBefore(vnodeToDom(newNode[i]), oldNode[index].dom) + else: + if positionByKey.hasKey(newNode[i].key): + var oldPos = positionByKey[newNode[i].key] + isNotRedundant[oldPos - leftOld] = true + current.appendChild(oldNode[oldPos].dom) + else: + current.appendChild(vnodeToDom(newNode[i])) + + # remove redundant old nodes + for i in leftOld..rightOld: + if not isNotRedundant[i]: + current.removeChild(oldNode[i].dom) + else: + # simply diff + print($leftOld & " " & $rightOld & " " & $leftNew & " " & $rightNew, ident) + while rightOld >= leftOld and rightNew >= leftNew: + updateElement(current, oldNode[leftOld].dom, newNode[leftNew], oldNode[leftOld], ident + 1) + inc leftNew + inc leftOld + + print("TEMP", ident) + print(printChildren(current), ident) + print($leftOld & " " & $rightOld & " " & $leftNew & " " & $rightNew, ident) + print("other part start", ident) + var isPushBack = (rightNew + 1 == newLength) + var nextNode: Node = nil + if not isPushBack: + print($(rightNew + 1), ident) + nextNode = newNode[rightNew + 1].dom + while leftNew <= rightNew: + var node = vnodeToDom(newNode[leftNew]) + if isPushBack: + current.appendChild(node) + else: + current.insertBefore(node, nextNode) + inc leftNew + print("other part finish", ident) - # var leftNew = left - # var leftOld = left - # while rightOld >= leftOld and rightNew >= leftNew and equalsShallow(oldNode[leftOld], newNode[rightNew]): - # var nextNode: Node = nil - # if rightNew + 1 < newLength: - # nextNode = newNode[rightNew + 1].dom - # if nextNode == nil: - # current.appendChild(oldNode[leftOld].dom) - # else: - # current.insertBefore(oldNode[leftOld].dom, nextNode) - # inc leftOld - # dec rightNew + for i in leftOld..rightOld: + current.removeChild(oldNode[i].dom) + detach(oldNode[i]) - # var lastTarget: Node = nil - # if leftOld <= rightOld: - # lastTarget = oldNode[leftOld].dom - # while rightOld >= leftOld and rightNew >= leftNew and equalsShallow(oldNode[rightOld], newNode[leftNew]): - # current.insertBefore(oldNode[rightOld].dom, lastTarget) - # lastTarget = oldNode[rightOld].dom - # inc leftNew - # dec rightOld - - # if rightOld < leftOld: - # var isPushBack = leftOld < oldLength - # for i in leftNew..rightNew: - # if isPushBack: - # current.appendChild(vnodeToDom(newNode[i])) - # else: - # current.insertBefore(vnodeToDom(newNode[i]), oldNode[leftOld].dom) - # elif rightNew < leftNew: - # for i in leftOld..rightOld: - # current.removeChild(oldNode[i].dom) - # detach(oldNode[i]) - # else: - # while rightOld >= leftOld and rightNew >= leftNew: - # updateElement(current, oldNode[leftOld].dom, newNode[leftNew], oldNode[leftOld]) - # inc leftNew - # inc leftOld - # if rightOld < leftOld: - # while rightNew >= leftNew: - # current.appendChild(vnodeToDom(newNode[leftNew])) - # inc leftNew - # elif rightNew < leftNew: - # while rightOld >= leftOld: - # current.removeChild(oldNode[leftOld].dom) - # detach(oldNode[leftOld]) - # inc leftOld + print("----------------", ident) + print("before", ident) + print(before, ident) + print("finish", ident) + print(printChildren(current), ident) + print("----------------", ident) + print("----------------", ident) when false: var drawTimeout: Timeout diff --git a/tests/diffDomTests.nim b/tests/diffDomTests.nim index 403980a..597f9b8 100644 --- a/tests/diffDomTests.nim +++ b/tests/diffDomTests.nim @@ -147,20 +147,20 @@ proc onload() = var dtTest = 500 var t = dtReset - # timeout = setTimeout(test1, t) - # t += dtTest - # timeout = setTimeout(reset, t) - # t += dtReset + timeout = setTimeout(test1, t) + t += dtTest + timeout = setTimeout(reset, t) + t += dtReset - # timeout = setTimeout(test2, t) - # t += dtTest - # timeout = setTimeout(reset, t) - # t += dtReset + timeout = setTimeout(test2, t) + t += dtTest + timeout = setTimeout(reset, t) + t += dtReset - # timeout = setTimeout(test3, t) - # t += dtTest - # timeout = setTimeout(reset, t) - # t += dtReset + timeout = setTimeout(test3, t) + t += dtTest + timeout = setTimeout(reset, t) + t += dtReset timeout = setTimeout(test4, t) t += dtTest @@ -172,12 +172,12 @@ proc onload() = timeout = setTimeout(reset, t) t += dtReset - # timeout = setTimeout(test6, t) - # t += dtTest - # timeout = setTimeout(reset, t) - # t += dtReset + timeout = setTimeout(test6, t) + t += dtTest + timeout = setTimeout(reset, t) + t += dtReset - # timeout = setTimeout(test7, t) + timeout = setTimeout(test7, t) onload() setRenderer createDom From e88f9e4c96bd5e6f21bc9469e116fd35b4639f10 Mon Sep 17 00:00:00 2001 From: Roman Vishnevskii Date: Wed, 10 May 2017 18:51:13 +0700 Subject: [PATCH 020/247] Fix empty LIS --- src/karax.nim | 29 ++++++++++++++++++++++++----- tests/diffDomTests.nim | 8 ++++---- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index e661b78..0e58c4b 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -1,6 +1,6 @@ ## Karax -- Single page applications for Nim. -import dom, vdom, jstrutils, components, jdict, tables +import dom, vdom, jstrutils, components, jdict export dom.Event @@ -184,6 +184,9 @@ proc print(s: cstring, ident: int) = # kout cstring(result) proc longestIncreasingSubsequence(a: seq[int]): seq[int] = + if len(a) == 0: + return @[] + result.add 0 var parent = newSeq[int](len(a)) for i in 0.. leftNew: # remove redundant old nodes @@ -325,14 +336,22 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode, ident: int = detach(oldNode[i]) else: # permute elements using LIS - var positionByKey = newTable[VKey, int]() + var positionByKey = newJDict[VKey, int]() var positions = newSeq[int]() for i in leftOld..rightOld: positionByKey[oldNode[i].key] = i for i in leftNew..rightNew: - if positionByKey.hasKey(newNode[i].key): + if positionByKey.contains(newNode[i].key): positions.add positionByKey[newNode[i].key] + #if len(positions) > 0: + # kout cstring("new segment len = " & $(rightNew - leftNew + 1)) + # kout cstring("old segment len = " & $(rightOld - leftOld + 1)) + # var t = "" + # for i in 0.. Date: Sat, 13 May 2017 12:57:53 +0200 Subject: [PATCH 021/247] test_diagram.xml --- experiments/Untitled Diagram.xml | 1 + 1 file changed, 1 insertion(+) create mode 100644 experiments/Untitled Diagram.xml diff --git a/experiments/Untitled Diagram.xml b/experiments/Untitled Diagram.xml new file mode 100644 index 0000000..fd1e674 --- /dev/null +++ b/experiments/Untitled Diagram.xml @@ -0,0 +1 @@ +UzV2zq1wL0osyPDNT0nNUTV2VTV2LsrPL4GwciucU3NyVI0MMlNUjV1UjYwMgFjVyA2HrCFY1qAgsSg1rwSLBiADYTaQg2Y1AA== \ No newline at end of file From 521ef9baecb61d1dce3b54f0e3d2d37bf664624d Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Sat, 13 May 2017 13:01:50 +0200 Subject: [PATCH 022/247] Update Untitled Diagram.xml --- experiments/Untitled Diagram.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experiments/Untitled Diagram.xml b/experiments/Untitled Diagram.xml index fd1e674..6f1b80e 100644 --- a/experiments/Untitled Diagram.xml +++ b/experiments/Untitled Diagram.xml @@ -1 +1 @@ -UzV2zq1wL0osyPDNT0nNUTV2VTV2LsrPL4GwciucU3NyVI0MMlNUjV1UjYwMgFjVyA2HrCFY1qAgsSg1rwSLBiADYTaQg2Y1AA== \ No newline at end of file +1VZNj5swEP01HLsCO4Hk2jRtD61UKYd2j16YgLuGQY4JpL++Bg8Bl+6qatVIySHyvBnPx3vWJAHfld0HLeriM2agAhZmXcDfBYwl68h+98DFAavtxgG5lpmDogk4yB9AYEhoIzM4eYEGURlZ+2CKVQWp8TChNbZ+2BGVX7UWOSyAQyrUEv0qM1M4dMPiCf8IMi/GylG8dZ4nkT7nGpuK6gWMH4ePc5dizEWDngqRYTuD+D7gO41o3KnsdqB6akfa3L33L3ivfWuozJ9cSKgNcxlHh8wyQSZqU2COlVD7CX07jAd9gtBahSmVPUb2CJ0033r4ga3JfBxdldEX51uP5iNl+A7GXOgNiMaghabCnxBrynGUSu1QoR4atbxCnKYWPxmNzzDzZMn2Kewzu+H6iV6kh6ATNjqlKEYPTugcKIpfpbEvHrAE274N0aCEkWc/u6C3l1/jJv7tgST4vRyb/yNH8oocK+YJ8iZ8CKNkRL6AlnYI0Pcj1epGUlHps1DNL8trpl5bSAOHWgz9tnZV+gr9LU1n0Aa614laUjBe4NQn7eYtme206KJxPRWzJReH/04aX5DG74Kz2Kfsur5vwdlq+dDYXZDG1rdjzZrTj+bgm/0x4fuf \ No newline at end of file From b44f699dd017d36c7bf2b24317e1611c9e2f6e6e Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Sat, 13 May 2017 13:09:27 +0200 Subject: [PATCH 023/247] Update Untitled Diagram.xml --- experiments/Untitled Diagram.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experiments/Untitled Diagram.xml b/experiments/Untitled Diagram.xml index 6f1b80e..ad93172 100644 --- a/experiments/Untitled Diagram.xml +++ b/experiments/Untitled Diagram.xml @@ -1 +1 @@ -1VZNj5swEP01HLsCO4Hk2jRtD61UKYd2j16YgLuGQY4JpL++Bg8Bl+6qatVIySHyvBnPx3vWJAHfld0HLeriM2agAhZmXcDfBYwl68h+98DFAavtxgG5lpmDogk4yB9AYEhoIzM4eYEGURlZ+2CKVQWp8TChNbZ+2BGVX7UWOSyAQyrUEv0qM1M4dMPiCf8IMi/GylG8dZ4nkT7nGpuK6gWMH4ePc5dizEWDngqRYTuD+D7gO41o3KnsdqB6akfa3L33L3ivfWuozJ9cSKgNcxlHh8wyQSZqU2COlVD7CX07jAd9gtBahSmVPUb2CJ0033r4ga3JfBxdldEX51uP5iNl+A7GXOgNiMaghabCnxBrynGUSu1QoR4atbxCnKYWPxmNzzDzZMn2Kewzu+H6iV6kh6ATNjqlKEYPTugcKIpfpbEvHrAE274N0aCEkWc/u6C3l1/jJv7tgST4vRyb/yNH8oocK+YJ8iZ8CKNkRL6AlnYI0Pcj1epGUlHps1DNL8trpl5bSAOHWgz9tnZV+gr9LU1n0Aa614laUjBe4NQn7eYtme206KJxPRWzJReH/04aX5DG74Kz2Kfsur5vwdlq+dDYXZDG1rdjzZrTj+bgm/0x4fuf \ No newline at end of file +1VdNc5swEP01HJtBEgZzresmh3amMz40OSqwBjUy65Hlr/76ChAGFdvjOLE95sBIb1cf+/ZpER4bzTaPis/zn5iC9Kifbjz2zaM0GhDzLoFtDQTxsAYyJdIaIi0wEX/Bgr5FlyKFheOoEaUWcxdMsCgg0Q7GlcK16zZF6a465xn0gEnCZR/9LVKd1+iQhi3+BCLLm5VJGNeWV568ZQqXhV3Po2xaPbV5xpu5bKCLnKe47kBs7LGRQtR1a7YZgSypbWirx30/YN3tW0GhTxkQ2W3obRM6pIYJ20Wlc8yw4HLcol+r8KCcwDe9XM+kaRLThI3QzyX8QAe2+9KYCq22tW3QdF/sDH9A663VAF9qNFC78A/EuZ1jKqQcoURVbdTwCmGSGHyhFb5Bx5JG8atfzlwHV0Z0kB4LLXCpEutFreC4ysB6sV1qjOIBZ2C2b1wUSK7Fyp2dW+1lO7+Wf9OwKdifjuFl0hEdSUdAnYR88R98EjXIL1DCBAHqflIVXClVdukVl8v/ilcne+tcaJjMebXftSmVbobOpWkFSsPmOFF9CpoBzO7T1ubYdtdtoSNNeco7RS70P07aHoq6Ai+wOEnRpCPnTkF5tiPa6lLZThXteRJkt5Mg60vQo2aUz+5CiKGrw9038SpCJD2KzhGi7wiROkokrhLp5aUY3O7DFeythqUWCb0LMdLBLdUY9ej7kDwvK7O4LzMSXkln5PgN6eRze/SGGgfUPbs+Y+9gde/N6dOovtaJjg+e6ODd95xPOJ+Bf8NbS3i0ut2Ok+H1apbptn+lla3z58/G/wA= \ No newline at end of file From 1f0c5d98dd38a5eabc06e81a904cff887805d890 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Sat, 13 May 2017 13:16:19 +0200 Subject: [PATCH 024/247] Update Untitled Diagram.xml --- experiments/Untitled Diagram.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experiments/Untitled Diagram.xml b/experiments/Untitled Diagram.xml index ad93172..a3019f8 100644 --- a/experiments/Untitled Diagram.xml +++ b/experiments/Untitled Diagram.xml @@ -1 +1 @@ -1VdNc5swEP01HJtBEgZzresmh3amMz40OSqwBjUy65Hlr/76ChAGFdvjOLE95sBIb1cf+/ZpER4bzTaPis/zn5iC9Kifbjz2zaM0GhDzLoFtDQTxsAYyJdIaIi0wEX/Bgr5FlyKFheOoEaUWcxdMsCgg0Q7GlcK16zZF6a465xn0gEnCZR/9LVKd1+iQhi3+BCLLm5VJGNeWV568ZQqXhV3Po2xaPbV5xpu5bKCLnKe47kBs7LGRQtR1a7YZgSypbWirx30/YN3tW0GhTxkQ2W3obRM6pIYJ20Wlc8yw4HLcol+r8KCcwDe9XM+kaRLThI3QzyX8QAe2+9KYCq22tW3QdF/sDH9A663VAF9qNFC78A/EuZ1jKqQcoURVbdTwCmGSGHyhFb5Bx5JG8atfzlwHV0Z0kB4LLXCpEutFreC4ysB6sV1qjOIBZ2C2b1wUSK7Fyp2dW+1lO7+Wf9OwKdifjuFl0hEdSUdAnYR88R98EjXIL1DCBAHqflIVXClVdukVl8v/ilcne+tcaJjMebXftSmVbobOpWkFSsPmOFF9CpoBzO7T1ubYdtdtoSNNeco7RS70P07aHoq6Ai+wOEnRpCPnTkF5tiPa6lLZThXteRJkt5Mg60vQo2aUz+5CiKGrw9038SpCJD2KzhGi7wiROkokrhLp5aUY3O7DFeythqUWCb0LMdLBLdUY9ej7kDwvK7O4LzMSXkln5PgN6eRze/SGGgfUPbs+Y+9gde/N6dOovtaJjg+e6ODd95xPOJ+Bf8NbS3i0ut2Ok+H1apbptn+lla3z58/G/wA= \ No newline at end of file +5VhNc9owEP01PjYjS8bGx4bQ9tDOZIZDk6OwN7YaYTFCfPXXV7ZkbGHDEEJgMuXASE+rlfR230rgkdFs813Sef5LpMA9jNKNRx48jKOBr79LYGuAIB4aIJMsNZDfABP2FyyILLpkKSwcQyUEV2zugokoCkiUg1Epxdo1exHcXXVOM+gAk4TyLvqbpSo36BCHDf4DWJbXK/thbEamNHnNpFgWdj0Pk5fqY4ZntPZlD7rIaSrWLYiMPTKSQijTmm1GwEtqa9rMvG8HRnf7llCoUyZEdhtqWx8dUs2E7QqpcpGJgvJxg95Xx4PSAdK9XM24bvq6CRumnkr4Dg9s97keKpTcmrFB3X22Hv6AUlubA3SphIaahX8KMbc+XhjnI8GFrDaqeYUwSTS+UFK8QmskjeIpKj2bw5UnOkiPhRZiKRNrhW3CUZmBtSK70OiMBzEDvX1tIoFTxVaud2pzL9vZNfzrhg1BfziGHxOO6Eg4AuwE5Au6Q35UI48gmT4EyM8TquBKobJLryhf7hWvVvTWOVMwmdNqv2tdKt0InUvTCqSCzXGiuhTUE4jdp63Nse2um0Ln1+UpbxW5EL2ftJqiFmseDrle4T5lK93MVHVIA03lPqL999j9D1Nny4XSxBX6Yug6mcLJ6+kLR+picfmTHalbhShOKlR1+9m9J57sjObSqMbOrUXDBPpFdj9EEYrOrkXk8rWomvpVSrptGcwFK9Si5fmxBFr6jl1948HeE2DPfifKfnvdMDtoBL47ykmaJ91C6WE9C5FPUS7DPXbwNcul36HoHF0hR1fYEZbvCgu/RVnn6SS43fMq6L2zy1z08adIRkJumY1Rz+X9jvT82DSLu2nmh1fKM//4O/5k3R79HRUH2NUuIuQNrPa+7y9G9bUUHR9UdPDm1/gF9BmgG76tw6PV7XacDK9Xs3S3+e/EvFWa/6fI+B8= \ No newline at end of file From 28f3e61b2e7680646ca81858196139140e8fe8bb Mon Sep 17 00:00:00 2001 From: Andre Smit Date: Mon, 15 May 2017 09:46:38 -0500 Subject: [PATCH 025/247] Add oncontextmenu event --- src/vdom.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vdom.nim b/src/vdom.nim index 25f6c12..1dc389e 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -41,6 +41,7 @@ type type EventKind* {.pure.} = enum ## The events supported by the virtual DOM. onclick, ## An element is clicked. + oncontextmenu, ## An element is right-clicked. ondblclick, ## An element is double clicked. onkeyup, ## A key was released. onkeydown, ## A key is pressed. From 9160776999e5b93d81b5c788694a2ee626efe99b Mon Sep 17 00:00:00 2001 From: Alexander Ivanov Date: Thu, 18 May 2017 10:00:39 +0300 Subject: [PATCH 026/247] Resolve a quote bug --- src/components.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components.nim b/src/components.nim index c3cda9c..7fcc707 100644 --- a/src/components.nim +++ b/src/components.nim @@ -79,8 +79,8 @@ proc stateDecl(n: NimNode; names: TableRef[string, bool]; decl: NimNode) = for i in 0 .. c.len-3: let v = $c[i] let sv = toState v - decl.add quote do: - var `sv` = newStateDict[`usedType`]() + decl.add(quote do: + var `sv` = newStateDict[`usedType`]()) if val.kind != nnkEmpty: decl.add newTree(nnkAsgn, newDotExpr(sv, newIdentNode"defaultValue"), val) From 084a1988c809d1aec7d2d587a011bb8d22e389ae Mon Sep 17 00:00:00 2001 From: Alexander Ivanov Date: Thu, 18 May 2017 10:01:02 +0300 Subject: [PATCH 027/247] Resolve a macro bug --- src/karaxdsl.nim | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index 5ba525a..faa43c3 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -101,20 +101,22 @@ proc tcall2(n, tmpContext: NimNode): NimNode = result = n macro buildHtml*(tag, children: untyped): VNode = - expectKind children, nnkDo + let kids = newProc(procType=nnkDo, body=children) + expectKind kids, nnkDo var call: NimNode if tag.kind in nnkCallKinds: call = tag else: call = newCall(tag) - call.add body(children) + call.add body(kids) result = tcall2(call, nil) when defined(debugKaraxDsl): echo repr result macro buildHtml*(children: untyped): VNode = - expectKind children, nnkDo - result = tcall2(body(children), nil) + let kids = newProc(procType=nnkDo, body=children) + expectKind kids, nnkDo + result = tcall2(body(kids), nil) when defined(debugKaraxDsl): echo repr result From 76a2031c37021a4dbf2ae43dadad689f57f91c28 Mon Sep 17 00:00:00 2001 From: Alexander Ivanov Date: Thu, 18 May 2017 12:58:43 +0300 Subject: [PATCH 028/247] Support submit and input event in the vdom --- src/vdom.nim | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vdom.nim b/src/vdom.nim index 1dc389e..af0c98a 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -73,6 +73,9 @@ type ondragstart, ## The user starts dragging an element or text selection. ondrop, ## An element is dropped on a valid drop target. + onsubmit, ## A form is submitted + oninput, ## An input value changes + onkeyupenter, ## vdom extension: an input field received the ENTER key press onkeyuplater ## vdom extension: a key was pressed and some time ## passed (useful for on-the-fly text completions) From d6c7be710673b2393ef849a055cc9a1cb01bb122 Mon Sep 17 00:00:00 2001 From: Araq Date: Thu, 18 May 2017 15:53:58 +0200 Subject: [PATCH 029/247] added vstyles karax module for inline CSS support --- experiments/example.nim | 12 +++- src/dom.nim | 2 +- src/karax.nim | 4 +- src/karaxdsl.nim | 2 +- src/vdom.nim | 6 +- src/vstyles.nim | 154 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 175 insertions(+), 5 deletions(-) create mode 100644 src/vstyles.nim diff --git a/experiments/example.nim b/experiments/example.nim index 229cce6..caa7285 100644 --- a/experiments/example.nim +++ b/experiments/example.nim @@ -1,5 +1,5 @@ -import vdom, components, karax, karaxdsl, jdict, jstrutils, dom +import vdom, vstyles, components, karax, karaxdsl, jdict, jstrutils, dom var images: seq[cstring] = @[cstring"a", "b", "c", "d"] @@ -24,10 +24,20 @@ proc carousel*(key: VKey): VNode {.component.} = clearTimeout(timer) timer = setTimeout(docount, 30) + let col = + case counter + of 0: cstring"#4d4d4d" + of 1: cstring"#ff00ff" + of 2: cstring"#00ffff" + of 3: cstring"#ffff00" + else: cstring"red" + result = buildHtml(tdiv(key=key)): text images[counter] button(onclick = next): text "Next" + tdiv(style = style(StyleAttr.color, col)): + text "This changes its color." if cntdown != 5: text &cntdown diff --git a/src/dom.nim b/src/dom.nim index f3d3f73..f2dabcf 100644 --- a/src/dom.nim +++ b/src/dom.nim @@ -188,7 +188,7 @@ type vspace*: int width*: int - Style = ref StyleObj + Style* = ref StyleObj StyleObj {.importc.} = object of RootObj background*: cstring backgroundAttachment*: cstring diff --git a/src/karax.nim b/src/karax.nim index ed956d8..2ac4e91 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -1,6 +1,6 @@ ## Karax -- Single page applications for Nim. -import dom, vdom, jstrutils, components, jdict +import dom, vdom, jstrutils, components, jdict, vstyles export dom.Event @@ -104,6 +104,7 @@ proc vnodeToDom(n: VNode): Node = wrapEvent(result, n, e, h) if n == toFocusV and toFocus.isNil: toFocus = result + if not n.style.isNil: applyStyle(result, n.style) proc same(n: VNode, e: Node): bool = if toTag[n.kind] == e.nodename: @@ -135,6 +136,7 @@ proc equalsShallow(a, b: VNode): bool = if not equalsShallow(a[i], b[i]): return false if not sameAttrs(a, b): return false if a.class != b.class: return false + if a.style != b.style: return false # XXX test event listeners here? return true diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index faa43c3..190545c 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -4,7 +4,7 @@ from strutils import startsWith, toLowerAscii const StmtContext = ["kout", "inc", "echo", "dec", "!"] - SpecialAttrs = ["id", "class", "value", "key"] + SpecialAttrs = ["id", "class", "value", "key", "style"] proc getName(n: NimNode): string = case n.kind diff --git a/src/vdom.nim b/src/vdom.nim index 1dc389e..9d2b90d 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -1,7 +1,7 @@ ## Virtual DOM implementation for Karax. from dom import Event, Node -import shash, macros +import shash, macros, vstyles from strutils import toUpperAscii type @@ -77,6 +77,7 @@ type onkeyuplater ## vdom extension: a key was pressed and some time ## passed (useful for on-the-fly text completions) + macro buildLookupTables(): untyped = var a = newTree(nnkBracket) for i in low(VNodeKind)..high(VNodeKind): @@ -99,6 +100,7 @@ buildLookupTables() type EventHandler* = proc (ev: Event; target: VNode) {.closure.} VKey* = int + VNode* = ref object kind*: VNodeKind key*: VKey @@ -109,6 +111,7 @@ type events*: seq[(EventKind, EventHandler)] hash*: Hash validHash*: bool + style*: VStyle ## the style that should be applied to the virtual node. dom*: Node ## the attached real DOM node. Can be 'nil' if the virtual node ## is not part of the virtual DOM anymore. @@ -143,6 +146,7 @@ proc eq*(a, b: VNode): bool = if a.attrs.len != b.attrs.len: return false for i in 0.. Date: Mon, 29 May 2017 10:59:59 +0200 Subject: [PATCH 030/247] renamed dom module to 'kdom' to avoid conflicts with Nim's stdlib dom module; it will further diverge from it anyway --- examples/mediaplayer/mediaplayer.nim | 2 +- examples/scrollapp/scrollapp.nim | 5 +++-- experiments/ajaxtest.nim | 2 +- experiments/example.nim | 2 +- src/components.nim | 2 +- src/karax.nim | 4 ++-- src/{dom.nim => kdom.nim} | 0 src/vdom.nim | 2 +- src/vstyles.nim | 2 +- tests/diffDomTests.nim | 2 +- 10 files changed, 12 insertions(+), 11 deletions(-) rename src/{dom.nim => kdom.nim} (100%) diff --git a/examples/mediaplayer/mediaplayer.nim b/examples/mediaplayer/mediaplayer.nim index 18179c1..890c078 100644 --- a/examples/mediaplayer/mediaplayer.nim +++ b/examples/mediaplayer/mediaplayer.nim @@ -1,5 +1,5 @@ -import karax, karaxdsl, vdom, dom, components +import karax, karaxdsl, vdom, kdom, components const Play = 0 diff --git a/examples/scrollapp/scrollapp.nim b/examples/scrollapp/scrollapp.nim index e5ef9c1..8ead611 100644 --- a/examples/scrollapp/scrollapp.nim +++ b/examples/scrollapp/scrollapp.nim @@ -1,7 +1,7 @@ ## Example that shows how to accomplish an "infinitely scrolling" app. include karaxprelude -import jstrutils, dom +import jstrutils, kdom, vstyles var entries: seq[cstring] = @[] for i in 1..500: @@ -16,7 +16,8 @@ proc scrollEvent(ev: Event; n: VNode) = proc createDom(): VNode = result = buildHtml(): - tdiv(onscroll=scrollEvent, style="height: 400px; overflow: scroll"): + tdiv(onscroll=scrollEvent, style=style( + (StyleAttr.height, cstring"400px"), (StyleAttr.overflow, cstring"scroll"))): for x in entries: tdiv: text x diff --git a/experiments/ajaxtest.nim b/experiments/ajaxtest.nim index 7af60cd..720b6b2 100644 --- a/experiments/ajaxtest.nim +++ b/experiments/ajaxtest.nim @@ -1,4 +1,4 @@ -import dom, kajax +import kdom, kajax proc cb(httpStatus: int, response: cstring) = echo "Worked!" diff --git a/experiments/example.nim b/experiments/example.nim index caa7285..fd1e208 100644 --- a/experiments/example.nim +++ b/experiments/example.nim @@ -1,5 +1,5 @@ -import vdom, vstyles, components, karax, karaxdsl, jdict, jstrutils, dom +import vdom, vstyles, components, karax, karaxdsl, jdict, jstrutils, kdom var images: seq[cstring] = @[cstring"a", "b", "c", "d"] diff --git a/src/components.nim b/src/components.nim index 7fcc707..555de77 100644 --- a/src/components.nim +++ b/src/components.nim @@ -1,6 +1,6 @@ ## Components in Karax are built by the ``.component`` macro annotation. -import macros, jdict, dom, vdom, tables, strutils +import macros, jdict, kdom, vdom, tables, strutils type StateDict*[V] = ref object diff --git a/src/karax.nim b/src/karax.nim index 2ac4e91..3915b7f 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -1,8 +1,8 @@ ## Karax -- Single page applications for Nim. -import dom, vdom, jstrutils, components, jdict, vstyles +import kdom, vdom, jstrutils, components, jdict, vstyles -export dom.Event +export kdom.Event proc kout*[T](x: T) {.importc: "console.log", varargs.} ## the preferred way of debugging karax applications. diff --git a/src/dom.nim b/src/kdom.nim similarity index 100% rename from src/dom.nim rename to src/kdom.nim diff --git a/src/vdom.nim b/src/vdom.nim index 9d2b90d..a10dce6 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -1,6 +1,6 @@ ## Virtual DOM implementation for Karax. -from dom import Event, Node +from kdom import Event, Node import shash, macros, vstyles from strutils import toUpperAscii diff --git a/src/vstyles.nim b/src/vstyles.nim index 022476e..f3d8dbc 100644 --- a/src/vstyles.nim +++ b/src/vstyles.nim @@ -1,5 +1,5 @@ -import dom, macros +import kdom, macros type StyleAttr* {.pure.} = enum ## the style attributes supported by the virtual DOM. diff --git a/tests/diffDomTests.nim b/tests/diffDomTests.nim index 7fdcf85..d2c09ab 100644 --- a/tests/diffDomTests.nim +++ b/tests/diffDomTests.nim @@ -1,5 +1,5 @@ -import dom, vdom, times, karax, karaxdsl, jdict, jstrutils, parseutils, sequtils +import kdom, vdom, times, karax, karaxdsl, jdict, jstrutils, parseutils, sequtils var entries: seq[cstring] From a9b5216a423c0e19c67d6e303d6441cb2a0c04d0 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 5 Jun 2017 10:21:38 +0200 Subject: [PATCH 031/247] diff algorithm special cases diffing of virtual styles --- src/karax.nim | 131 +++++++++++++++++++++++++++----------------------- 1 file changed, 72 insertions(+), 59 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 3915b7f..198a244 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -123,22 +123,26 @@ proc replaceById(id: cstring; newTree: Node) = x.parentNode.replaceChild(newTree, x) #newTree.id = id -proc equalsShallow(a, b: VNode): bool = - if a.kind != b.kind: return false - if a.id != b.id: return false - if a.key != b.key: return false +type + EqResult = enum + different, similar, identical + +proc equalsShallow(a, b: VNode): EqResult = + if a.kind != b.kind: return different + if a.id != b.id: return different + if a.key != b.key: return different if a.kind == VNodeKind.text: - if a.text != b.text: return false + if a.text != b.text: return different elif a.kind == VNodeKind.vthunk or a.kind == VNodeKind.dthunk: - if a.text != b.text: return false - if a.len != b.len: return false + if a.text != b.text: return different + if a.len != b.len: return different for i in 0.. oldLength: - for i in oldLength..newLength-1: - current.appendChild(vnodeToDom(newNode[i])) - elif oldLength > newLength: - for i in countdown(oldLength-1, newLength): + else: + if res == similar: + # we keep the oldNode, but take over the style from the new node: + if oldNode.dom != nil: + if newNode.style != nil: applyStyle(oldNode.dom, newNode.style) + else: oldNode.dom.style = Style() + oldNode.style = newNode.style + + if newNode.kind != VNodeKind.text: + let newLength = newNode.len + var oldLength = oldNode.len + let minLength = min(newLength, oldLength) + assert oldNode.kind == newNode.kind + when defined(simpleDiff): + for i in 0..min(newLength, oldLength)-1: + updateElement(current, current[i], newNode[i], oldNode[i]) + if newLength > oldLength: + for i in oldLength..newLength-1: + current.appendChild(vnodeToDom(newNode[i])) + elif oldLength > newLength: + for i in countdown(oldLength-1, newLength): + detach(oldNode[i]) + current.removeChild(current.lastChild) + else: + var commonPrefix = 0 + while commonPrefix < minLength and + equalsTree(newNode[commonPrefix], oldNode[commonPrefix]): + inc commonPrefix + + var oldPos = oldLength - 1 + var newPos = newLength - 1 + while oldPos >= commonPrefix and newPos >= commonPrefix and + equalsTree(newNode[newPos], oldNode[oldPos]): + dec oldPos + dec newPos + + var pos = min(oldPos, newPos) + 1 + for i in commonPrefix..pos-1: + updateElement(current, current.childNodes[i], newNode[i], oldNode[i]) + + var nextChildPos = oldPos + 1 + while pos <= newPos: + if nextChildPos == oldLength: + current.appendChild(vnodeToDom(newNode[pos])) + else: + current.insertBefore(vnodeToDom(newNode[pos]), current.childNodes[nextChildPos]) + # added new Node, so old state of VDOM have one more Node + inc oldLength + inc pos + inc nextChildPos + + for i in pos..oldPos: detach(oldNode[i]) - current.removeChild(current.lastChild) - else: - var commonPrefix = 0 - while commonPrefix < minLength and - equalsTree(newNode[commonPrefix], oldNode[commonPrefix]): - inc commonPrefix - - var oldPos = oldLength - 1 - var newPos = newLength - 1 - while oldPos >= commonPrefix and newPos >= commonPrefix and - equalsTree(newNode[newPos], oldNode[oldPos]): - dec oldPos - dec newPos - - var pos = min(oldPos, newPos) + 1 - for i in commonPrefix..pos-1: - updateElement(current, current.childNodes[i], newNode[i], oldNode[i]) - - var nextChildPos = oldPos + 1 - while pos <= newPos: - if nextChildPos == oldLength: - current.appendChild(vnodeToDom(newNode[pos])) - else: - current.insertBefore(vnodeToDom(newNode[pos]), current.childNodes[nextChildPos]) - # added new Node, so old state of VDOM have one more Node - inc oldLength - inc pos - inc nextChildPos - - for i in pos..oldPos: - detach(oldNode[i]) - current.removeChild(current.childNodes[pos]) + current.removeChild(current.childNodes[pos]) when false: var drawTimeout: Timeout From 06f528370979d9eaa94a765fbf9c4ad3951f654c Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 5 Jun 2017 10:25:46 +0200 Subject: [PATCH 032/247] vstyle diff: performance improvement --- src/vdom.nim | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vdom.nim b/src/vdom.nim index a10dce6..7cab49a 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -146,7 +146,9 @@ proc eq*(a, b: VNode): bool = if a.attrs.len != b.attrs.len: return false for i in 0.. Date: Mon, 5 Jun 2017 17:06:58 +0200 Subject: [PATCH 033/247] stuff --- src/karax.nim | 80 ++++++++++++++++++++++++++++++++------------------- src/vdom.nim | 18 ------------ 2 files changed, 51 insertions(+), 47 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 198a244..555b3bb 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -127,33 +127,35 @@ type EqResult = enum different, similar, identical -proc equalsShallow(a, b: VNode): EqResult = +proc eq(a, b: VNode; shallow: bool): EqResult = if a.kind != b.kind: return different if a.id != b.id: return different - if a.key != b.key: return different + result = identical + if a.key != b.key: + kout cstring"different keys", a.key, b.key + if b.key == 0: + kout cstring"key for ", cstring($b.kind) + return different if a.kind == VNodeKind.text: if a.text != b.text: return different elif a.kind == VNodeKind.vthunk or a.kind == VNodeKind.dthunk: if a.text != b.text: return different if a.len != b.len: return different for i in 0..= 0 and isDirty(newNode.key): @@ -170,8 +172,21 @@ proc updateDirtyElements(parent, current: Node, newNode: VNode) = # leave early if we know there cannot be anything left to do: #if dirtyCount <= 0: return +proc updateStyles(newNode, oldNode: VNode; shallow: bool) = + # we keep the oldNode, but take over the style from the new node: + if oldNode.dom != nil: + kout cstring"updateStyle deep updated dom" + if newNode.style != nil: applyStyle(oldNode.dom, newNode.style) + else: oldNode.dom.style = Style() + oldNode.style = newNode.style + if not shallow: + assert newNode.len == oldNode.len + for i in 0 ..< newNode.len: + updateStyles(newNode[i], oldNode[i], shallow) + proc updateElement(parent, current: Node, newNode, oldNode: VNode) = - let res = equalsShallow(newNode, oldNode) + let res = eq(newNode, oldNode, shallow=true) + kout cstring($res), cstring"shallow" if res == different: detach(oldNode) let n = vnodeToDom(newNode) @@ -180,12 +195,7 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) = else: parent.replaceChild(n, current) else: - if res == similar: - # we keep the oldNode, but take over the style from the new node: - if oldNode.dom != nil: - if newNode.style != nil: applyStyle(oldNode.dom, newNode.style) - else: oldNode.dom.style = Style() - oldNode.style = newNode.style + if res == similar: updateStyles(newNode, oldNode, true) if newNode.kind != VNodeKind.text: let newLength = newNode.len @@ -204,16 +214,27 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) = current.removeChild(current.lastChild) else: var commonPrefix = 0 - while commonPrefix < minLength and - equalsTree(newNode[commonPrefix], oldNode[commonPrefix]): - inc commonPrefix + + template eqAndUpdate(a, b: VNode; action: untyped) = + let r = eq(a, b, false) + kout cstring($r), cstring"eqAndUpdate" + case r + of identical: action + of different: break + of similar: + updateStyles(a, b, false) + action + + while commonPrefix < minLength: + eqAndUpdate(newNode[commonPrefix], oldNode[commonPrefix]): + inc commonPrefix var oldPos = oldLength - 1 var newPos = newLength - 1 - while oldPos >= commonPrefix and newPos >= commonPrefix and - equalsTree(newNode[newPos], oldNode[oldPos]): - dec oldPos - dec newPos + while oldPos >= commonPrefix and newPos >= commonPrefix: + eqAndUpdate(newNode[newPos], oldNode[oldPos]): + dec oldPos + dec newPos var pos = min(oldPos, newPos) + 1 for i in commonPrefix..pos-1: @@ -241,6 +262,7 @@ proc dodraw() = if dorender.isNil: return let newtree = dorender() newtree.id = "ROOT" + kout cstring"do draw!", cstring getStackTrace() toFocus = nil if currentTree == nil: currentTree = newtree diff --git a/src/vdom.nim b/src/vdom.nim index 7cab49a..4275c7b 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -133,24 +133,6 @@ proc vthunk*(name: cstring; args: varargs[VNode, vn]): VNode = proc dthunk*(name: cstring; args: varargs[VNode, vn]): VNode = VNode(kind: VNodeKind.dthunk, text: name, key: -1, kids: @args) -proc eq*(a, b: VNode): bool = - if a.kind != b.kind: return false - if a.id != b.id: return false - if a.class != b.class: return false - if a.key != b.key: return false - if a.kind != VNodeKind.text: - if a.kids.len != b.kids.len: return false - for i in 0.. Date: Mon, 5 Jun 2017 21:31:26 +0200 Subject: [PATCH 034/247] new diff algorithm works --- src/karax.nim | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 555b3bb..3fa2d7e 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -127,26 +127,22 @@ type EqResult = enum different, similar, identical -proc eq(a, b: VNode; shallow: bool): EqResult = +proc eq(a, b: VNode; deep: bool): EqResult = if a.kind != b.kind: return different if a.id != b.id: return different result = identical - if a.key != b.key: - kout cstring"different keys", a.key, b.key - if b.key == 0: - kout cstring"key for ", cstring($b.kind) - return different + if a.key != b.key: return different if a.kind == VNodeKind.text: if a.text != b.text: return different elif a.kind == VNodeKind.vthunk or a.kind == VNodeKind.dthunk: if a.text != b.text: return different if a.len != b.len: return different for i in 0.. Date: Mon, 5 Jun 2017 23:20:22 +0200 Subject: [PATCH 035/247] diff algorithm bugfix: don't be too smart in key handling --- src/karax.nim | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 3fa2d7e..1445b1b 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -75,12 +75,12 @@ proc vnodeToDom(n: VNode): Node = elif n.kind == VNodeKind.vthunk: let x = callThunk(vcomponents[n.text], n) result = vnodeToDom(x) - n.key = result.key + #n.key = result.key attach n return result elif n.kind == VNodeKind.dthunk: result = callThunk(dcomponents[n.text], n) - n.key = result.key + #n.key = result.key attach n return result else: @@ -95,8 +95,8 @@ proc vnodeToDom(n: VNode): Node = result.id = n.id if n.class != nil: result.class = n.class - if n.key >= 0: - result.key = n.key + #if n.key >= 0: + # result.key = n.key for k, v in attrs(n): if v != nil: result.setAttr(k, v) From fca6b2a6ca1ef5b4ebad9d431742a5247ccca9ae Mon Sep 17 00:00:00 2001 From: Fabian Keller Date: Tue, 6 Jun 2017 20:02:46 +0200 Subject: [PATCH 036/247] added all css styles to close #20 --- src/vstyles.nim | 121 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 112 insertions(+), 9 deletions(-) diff --git a/src/vstyles.nim b/src/vstyles.nim index f3d8dbc..649c322 100644 --- a/src/vstyles.nim +++ b/src/vstyles.nim @@ -2,51 +2,112 @@ import kdom, macros type - StyleAttr* {.pure.} = enum ## the style attributes supported by the virtual DOM. + StyleAttr* {.pure.} = enum + ## The style attributes supported by the virtual DOM. + ## Reference: https://www.w3schools.com/jsref/dom_obj_style.asp + alignContent + alignItems + alignSelf + animation + animationDelay + animationDirection + animationDuration + animationFillMode + animationIterationCount + animationName + animationTimingFunction + animationPlayState background backgroundAttachment backgroundColor backgroundImage backgroundPosition backgroundRepeat + backgroundClip + backgroundOrigin + backgroundSize + backfaceVisibility border borderBottom borderBottomColor + borderBottomLeftRadius + borderBottomRightRadius borderBottomStyle borderBottomWidth + borderCollapse borderColor + borderImage + borderImageOutset + borderImageRepeat + borderImageSlice + borderImageSource + borderImageWidth borderLeft borderLeftColor borderLeftStyle borderLeftWidth + borderRadius borderRight borderRightColor borderRightStyle borderRightWidth + borderSpacing borderStyle borderTop borderTopColor + borderTopLeftRadius + borderTopRightRadius borderTopStyle borderTopWidth borderWidth bottom + boxDecorationBreak + boxShadow + boxSizing captionSide clear clip color + columnCount + columnFill + columnGap + columnRule + columnRuleColor + columnRuleStyle + columnRuleWidth + columns + columnSpan + columnWidth + content + counterIncrement + counterReset cursor direction display emptyCells + filter + flex + flexBasis + flexDirection + flexFlow + flexGrow + flexShrink + flexWrap cssFloat font fontFamily fontSize + fontSizeAdjust fontStretch fontStyle fontVariant fontWeight + hangingPunctuation height + hyphens + icon + imageOrientation + justifyContent left letterSpacing lineHeight @@ -63,7 +124,22 @@ type maxWidth minHeight minWidth + navDown + navIndex + navLeft + navRight + navUp + opacity + order + orphans + outline + outlineColor + outlineOffset + outlineStyle + outlineWidth overflow + overflowX + overflowY padding paddingBottom paddingLeft @@ -71,26 +147,53 @@ type paddingTop pageBreakAfter pageBreakBefore + pageBreakInside + perspective + perspectiveOrigin position + quotes + resize right - scrollbar3dLightColor - scrollbarArrowColor - scrollbarBaseColor - scrollbarDarkshadowColor - scrollbarFaceColor - scrollbarHighlightColor - scrollbarShadowColor - scrollbarTrackColor + scrollbar3dLightColor # note: missing in w3schools reference + scrollbarArrowColor # note: missing in w3schools reference + scrollbarBaseColor # note: missing in w3schools reference + scrollbarDarkshadowColor # note: missing in w3schools reference + scrollbarFaceColor # note: missing in w3schools reference + scrollbarHighlightColor # note: missing in w3schools reference + scrollbarShadowColor # note: missing in w3schools reference + scrollbarTrackColor # note: missing in w3schools reference tableLayout + tabSize textAlign + textAlignLast textDecoration + textDecorationColor + textDecorationLine + textDecorationStyle textIndent + textJustify + textOverflow + textShadow textTransform top + transform + transformOrigin + transformStyle + transition + transitionDelay + transitionDuration + transitionProperty + transitionTimingFunction + unicodeBidi + userSelect verticalAlign visibility + whiteSpace width + wordBreak wordSpacing + wordWrap + widows zIndex macro buildLookupTables(): untyped = From e49fa7edf7ff285b55ef3e37999fc39914a66548 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 7 Jun 2017 10:07:37 +0200 Subject: [PATCH 037/247] disable smart vstyle diffing for now --- src/karax.nim | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/karax.nim b/src/karax.nim index 1445b1b..f8929e1 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -149,7 +149,8 @@ proc eq(a, b: VNode; deep: bool): EqResult = result = similar if not sameAttrs(a, b): return different if a.class != b.class: return different - if a.style != b.style: return similar + # XXX Fixme: smart diffing still is broken for complex apps: + if a.style != b.style: return different # similar # Do not test event listeners here! return result From dc5ba1f6461d90439eafb19b4fb5d173690f5482 Mon Sep 17 00:00:00 2001 From: Pavel Nuzhdin Date: Fri, 9 Jun 2017 15:44:16 +0700 Subject: [PATCH 038/247] Added stopPropagation event method --- src/kdom.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/src/kdom.nim b/src/kdom.nim index f2dabcf..349f02d 100644 --- a/src/kdom.nim +++ b/src/kdom.nim @@ -520,6 +520,7 @@ proc setAttribute*(s: Style, attr, value: cstring, caseSensitive=false) # Event "methods" proc preventDefault*(ev: Event) +proc stopPropagation*(ev: Event) # TouchEvent "methods" proc identifiedTouch*(list: TouchList): Touch From d5596c8ff50af6002ac24b26a63b0b451627ca6a Mon Sep 17 00:00:00 2001 From: Fabian Keller Date: Fri, 19 May 2017 20:15:07 +0200 Subject: [PATCH 039/247] added post render callback --- src/karax.nim | 8 +++++++- src/kdom.nim | 7 ++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index f8929e1..cc52e2f 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -117,6 +117,7 @@ proc same(n: VNode, e: Node): bool = var dorender: proc (): VNode {.closure.} currentTree: VNode + postRenderCallback: proc () proc replaceById(id: cstring; newTree: Node) = let x = document.getElementById(id) @@ -279,6 +280,10 @@ proc dodraw() = updateDirtyElements(nil, olddom, newtree) someDirty = false currentTree = newtree + + if not postRenderCallback.isNil: + postRenderCallback() + # now that it's part of the DOM, give it the focus: if toFocus != nil: toFocus.focus() @@ -299,9 +304,10 @@ proc redraw*() = proc init(ev: Event) = reqFrame(dodraw) -proc setRenderer*(renderer: proc (): VNode) = +proc setRenderer*(renderer: proc (): VNode, clientPostRenderCallback: proc () = nil) = dorender = renderer window.onload = init + postRenderCallback = clientPostRenderCallback proc addEventHandler*(n: VNode; k: EventKind; action: EventHandler) = ## Implements the foundation of Karax's event management. diff --git a/src/kdom.nim b/src/kdom.nim index 349f02d..0adb1dd 100644 --- a/src/kdom.nim +++ b/src/kdom.nim @@ -273,6 +273,7 @@ type textDecoration*: cstring textIndent*: cstring textTransform*: cstring + transform*: cstring top*: cstring verticalAlign*: cstring visibility*: cstring @@ -567,7 +568,7 @@ proc getElementsByClass*(n: Node; name: cstring): seq[Node] {. type BoundingRect* {.importc.} = object - top*, bottom*, left*, right*: int + top*, bottom*, left*, right*, x*, y*, width*, height*: float proc getBoundingClientRect*(e: Node): BoundingRect {. importcpp: "getBoundingClientRect", nodecl.} @@ -579,8 +580,8 @@ proc clientWidth*(): int {. proc inViewport*(el: Node): bool = let rect = el.getBoundingClientRect() result = rect.top >= 0 and rect.left >= 0 and - rect.bottom <= clientHeight() and - rect.right <= clientWidth() + rect.bottom <= clientHeight().float and + rect.right <= clientWidth().float proc scrollTop*(e: Node): int {.importcpp: "#.scrollTop", nodecl.} proc offsetHeight*(e: Node): int {.importcpp: "#.offsetHeight", nodecl.} From 14660f9d685d1b14cdead9cb09421f0f8ff7512e Mon Sep 17 00:00:00 2001 From: Fabian Keller Date: Thu, 15 Jun 2017 13:53:53 +0200 Subject: [PATCH 040/247] implemented refs draft --- experiments/refs/demo.html | 13 +++++++++++++ experiments/refs/demo.nim | 39 ++++++++++++++++++++++++++++++++++++++ experiments/refs/demo.nims | 1 + src/karax.nim | 10 ++++++++-- src/karaxdsl.nim | 2 +- src/vdom.nim | 4 ++++ 6 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 experiments/refs/demo.html create mode 100644 experiments/refs/demo.nim create mode 100644 experiments/refs/demo.nims diff --git a/experiments/refs/demo.html b/experiments/refs/demo.html new file mode 100644 index 0000000..e1fe419 --- /dev/null +++ b/experiments/refs/demo.html @@ -0,0 +1,13 @@ + + + + + + Refs + + + +
+ + + diff --git a/experiments/refs/demo.nim b/experiments/refs/demo.nim new file mode 100644 index 0000000..dffb1e9 --- /dev/null +++ b/experiments/refs/demo.nim @@ -0,0 +1,39 @@ +include karaxprelude +import random + +var + modelData = @[5, 2, 4] + + refA = VNodeRef() + refB = VNodeRef() + refSeq = newSeq[VNodeRef]() + +proc onClick(ev: Event, n: VNode) = + kout(refA) + kout(refB) + kout(refSeq) + +proc secureRefSlot(i: int): VNodeRef = + while refSeq.len <= i: + refSeq.add(VNodeRef()) + result = refSeq[i] + +proc view(): VNode = + result = buildHtml(): + tdiv: + button(onclick=onClick): + text "click me" + # storing refs to single elements is straightforward now + tdiv(nref=refA): + text "A" + tdiv(nref=refB): + text "B" + # It's a bit more tricky when containers are involved: + for i, x in modelData.pairs: + tdiv(nref=secureRefSlot(i)): + text "List item: " & $x + +proc renderer(): VNode = + view() + +setRenderer renderer diff --git a/experiments/refs/demo.nims b/experiments/refs/demo.nims new file mode 100644 index 0000000..9028ff1 --- /dev/null +++ b/experiments/refs/demo.nims @@ -0,0 +1 @@ +--path: "../../src" diff --git a/src/karax.nim b/src/karax.nim index cc52e2f..d8a4cb1 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -65,8 +65,14 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind; action: EventHandler) = # --------------------- DOM diff ----------------------------------------- -template detach(n: VNode) = n.dom = nil -template attach(n: Vnode) = n.dom = result +template detach(n: VNode) = + if not n.nref.isNil(): + n.nref.vnode = nil + n.dom = nil +template attach(n: VNode) = + if not n.nref.isNil(): + n.nref.vnode = n + n.dom = result proc vnodeToDom(n: VNode): Node = if n.kind == VNodeKind.text: diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index 190545c..8693f5b 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -4,7 +4,7 @@ from strutils import startsWith, toLowerAscii const StmtContext = ["kout", "inc", "echo", "dec", "!"] - SpecialAttrs = ["id", "class", "value", "key", "style"] + SpecialAttrs = ["id", "class", "value", "key", "style", "nref"] proc getName(n: NimNode): string = case n.kind diff --git a/src/vdom.nim b/src/vdom.nim index d55e7b4..f4ccff8 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -101,6 +101,9 @@ macro buildLookupTables(): untyped = buildLookupTables() type + VNodeRef* = ref object + vnode*: VNode + EventHandler* = proc (ev: Event; target: VNode) {.closure.} VKey* = int @@ -108,6 +111,7 @@ type kind*: VNodeKind key*: VKey id*, class*, text*: cstring + nref*: VNodeRef kids: seq[VNode] # even index: key, odd index: value; done this way for memory efficiency: attrs: seq[cstring] From 472f1deb58cb1ae718e395034e1e4099cf53c7c9 Mon Sep 17 00:00:00 2001 From: Fabian Keller Date: Thu, 15 Jun 2017 14:38:39 +0200 Subject: [PATCH 041/247] cleaned up example --- experiments/refs/demo.nim | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/experiments/refs/demo.nim b/experiments/refs/demo.nim index dffb1e9..cfbf7a3 100644 --- a/experiments/refs/demo.nim +++ b/experiments/refs/demo.nim @@ -1,5 +1,5 @@ include karaxprelude -import random +import future, sequtils var modelData = @[5, 2, 4] @@ -9,9 +9,9 @@ var refSeq = newSeq[VNodeRef]() proc onClick(ev: Event, n: VNode) = - kout(refA) - kout(refB) - kout(refSeq) + kout(refA.vnode) + kout(refB.vnode) + kout(refSeq.map(nref => nref.vnode)) proc secureRefSlot(i: int): VNodeRef = while refSeq.len <= i: From e33b9dc09a2242b6b85c027ebc76d0ba982018b2 Mon Sep 17 00:00:00 2001 From: Fabian Keller Date: Thu, 15 Jun 2017 19:01:15 +0200 Subject: [PATCH 042/247] added base methods for VNodeRef --- experiments/refs/demo.nim | 31 ++++++++++++++++++++++++------- src/karax.nim | 2 ++ src/vdom.nim | 6 +++++- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/experiments/refs/demo.nim b/experiments/refs/demo.nim index cfbf7a3..6ecc7db 100644 --- a/experiments/refs/demo.nim +++ b/experiments/refs/demo.nim @@ -1,16 +1,30 @@ include karaxprelude import future, sequtils +type + CustomRef = ref object of VNodeRef + +method onAttach(r: CustomRef) = + kout(cstring"custom ref attached") + +method onDetach(r: CustomRef) = + kout(cstring"custom ref detached") + var modelData = @[5, 2, 4] refA = VNodeRef() refB = VNodeRef() + refC = CustomRef() refSeq = newSeq[VNodeRef]() proc onClick(ev: Event, n: VNode) = - kout(refA.vnode) - kout(refB.vnode) + modelData.add(0) + +proc showRefs() = + kout(refA) + kout(refB) + kout(refC) kout(refSeq.map(nref => nref.vnode)) proc secureRefSlot(i: int): VNodeRef = @@ -24,10 +38,13 @@ proc view(): VNode = button(onclick=onClick): text "click me" # storing refs to single elements is straightforward now - tdiv(nref=refA): - text "A" - tdiv(nref=refB): - text "B" + if modelData.len mod 2 == 0: + tdiv(nref=refA): + text "A" + tdiv(nref=refB): + text "B" + tdiv(nref=refC): + text "C" # It's a bit more tricky when containers are involved: for i, x in modelData.pairs: tdiv(nref=secureRefSlot(i)): @@ -36,4 +53,4 @@ proc view(): VNode = proc renderer(): VNode = view() -setRenderer renderer +setRenderer(renderer, showRefs) diff --git a/src/karax.nim b/src/karax.nim index d8a4cb1..b454050 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -68,10 +68,12 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind; action: EventHandler) = template detach(n: VNode) = if not n.nref.isNil(): n.nref.vnode = nil + n.nref.onDetach() n.dom = nil template attach(n: VNode) = if not n.nref.isNil(): n.nref.vnode = n + n.nref.onAttach() n.dom = result proc vnodeToDom(n: VNode): Node = diff --git a/src/vdom.nim b/src/vdom.nim index f4ccff8..9f9c642 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -101,7 +101,7 @@ macro buildLookupTables(): untyped = buildLookupTables() type - VNodeRef* = ref object + VNodeRef* = ref object of RootObj vnode*: VNode EventHandler* = proc (ev: Event; target: VNode) {.closure.} @@ -122,6 +122,10 @@ type dom*: Node ## the attached real DOM node. Can be 'nil' if the virtual node ## is not part of the virtual DOM anymore. +method onAttach*(v: VNodeRef) {.base.} = discard +method onDetach*(v: VNodeRef) {.base.} = discard +method changed*(v: VNodeRef): bool {.base.} = false + proc value*(n: VNode): cstring = n.text proc `value=`*(n: VNode; v: cstring) = n.text = v From 08538f7f2675814436c60de3316323c7991692ef Mon Sep 17 00:00:00 2001 From: Araq Date: Fri, 16 Jun 2017 16:45:06 +0200 Subject: [PATCH 043/247] move parseInt to jstrutils.nim to avoid conflicts; introduce travis testing --- .travis.yml | 18 ++++++++++++++++++ src/jstrutils.nim | 1 + src/kdom.nim | 3 --- tests/tester.nim | 15 +++++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 .travis.yml create mode 100644 tests/tester.nim diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..0c60bd2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +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 + - cd .. +before_script: + - set -e + - export PATH=$(pwd)/nim/bin:$(pwd):$PATH +script: + - nim c -r tests/tester.nim diff --git a/src/jstrutils.nim b/src/jstrutils.nim index 0be4076..a16473d 100644 --- a/src/jstrutils.nim +++ b/src/jstrutils.nim @@ -23,3 +23,4 @@ proc isInt*(s: cstring): bool {.asmNoStackFrame.} = """ proc parseInt*(s: cstring): int {.importcpp: "parseInt(#, 10)", nodecl.} +proc parseFloat*(s: cstring): BiggestFloat {.importc, nodecl.} diff --git a/src/kdom.nim b/src/kdom.nim index 0adb1dd..f933b87 100644 --- a/src/kdom.nim +++ b/src/kdom.nim @@ -547,9 +547,6 @@ proc decodeURIComponent*(uri: cstring): cstring {.importc, nodecl.} proc encodeURIComponent*(uri: cstring): cstring {.importc, nodecl.} proc isFinite*(x: BiggestFloat): bool {.importc, nodecl.} proc isNaN*(x: BiggestFloat): bool {.importc, nodecl.} -proc parseFloat*(s: cstring): BiggestFloat {.importc, nodecl.} -proc parseInt*(s: cstring): int {.importc, nodecl.} -proc parseInt*(s: cstring, radix: int):int {.importc, nodecl.} proc id*(n: Node): cstring {.importcpp: "#.id", nodecl.} diff --git a/tests/tester.nim b/tests/tester.nim new file mode 100644 index 0000000..5dc7426 --- /dev/null +++ b/tests/tester.nim @@ -0,0 +1,15 @@ +# For now we only test that the things still compile. + +import os + +proc exec(cmd: string) = + if os.execShellCmd(cmd) != 0: + quit "command failed " & cmd + +proc main = + exec("nim js tests/diffDomTests.nim") + exec("nim js examples/todoapp/todoapp.nim") + exec("nim js examples/scrollapp/scrollapp.nim") + exec("nim js examples/mediaplayer/playerapp.nim") + +main() From b3fba429ad89ee45419e9c3555bdbd53a9c53ba7 Mon Sep 17 00:00:00 2001 From: Araq Date: Fri, 16 Jun 2017 18:02:35 +0200 Subject: [PATCH 044/247] introduce KaraxInstance; 100% backwards compatible --- src/karax.nim | 121 ++++++++++++++++++++++++++--------------------- src/karaxdsl.nim | 4 +- 2 files changed, 70 insertions(+), 55 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index cc52e2f..d9f5773 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -14,12 +14,26 @@ proc key*(e: Node): VKey = else: result = -1 proc `key=`*(e: Node; x: VKey) {.importcpp: "#.karaxKey = #", nodecl.} -var - toFocus: Node - toFocusV: VNode +type + KaraxInstance* = ref object ## underlying karax instance. Usually you don't have + ## know about this. + rootId: cstring not nil + renderer: proc (): VNode {.closure.} + currentTree: VNode + postRenderCallback: proc () + toFocus: Node + toFocusV: VNode -proc setFocus*(n: VNode) = - toFocusV = n +var + kxi*: KaraxInstance ## The current Karax instance. This is always used + ## as the default. **Note**: Within the karax DSL + ## always a symbol of the name *kxi* is assumed, so + ## if you have a local karax instance to use instead + ## in your 'buildHtml' statement, it needs to be named + ## 'kxi'. + +proc setFocus*(n: VNode; kxi: KaraxInstance = kxi) = + kxi.toFocusV = n # ----------------- event wrapping --------------------------------------- @@ -68,13 +82,13 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind; action: EventHandler) = template detach(n: VNode) = n.dom = nil template attach(n: Vnode) = n.dom = result -proc vnodeToDom(n: VNode): Node = +proc vnodeToDom(n: VNode; kxi: KaraxInstance): Node = if n.kind == VNodeKind.text: result = document.createTextNode(n.text) attach n elif n.kind == VNodeKind.vthunk: let x = callThunk(vcomponents[n.text], n) - result = vnodeToDom(x) + result = vnodeToDom(x, kxi) #n.key = result.key attach n return result @@ -87,7 +101,7 @@ proc vnodeToDom(n: VNode): Node = result = document.createElement(toTag[n.kind]) attach n for k in n: - appendChild(result, vnodeToDom(k)) + appendChild(result, vnodeToDom(k, kxi)) # text is mapped to 'value': if n.text != nil: result.value = n.text @@ -102,8 +116,8 @@ proc vnodeToDom(n: VNode): Node = result.setAttr(k, v) for e, h in items(n.events): wrapEvent(result, n, e, h) - if n == toFocusV and toFocus.isNil: - toFocus = result + if n == kxi.toFocusV and kxi.toFocus.isNil: + kxi.toFocus = result if not n.style.isNil: applyStyle(result, n.style) proc same(n: VNode, e: Node): bool = @@ -114,11 +128,6 @@ proc same(n: VNode, e: Node): bool = for i in 0 ..< n.len: if not same(n[i], e[i]): return false -var - dorender: proc (): VNode {.closure.} - currentTree: VNode - postRenderCallback: proc () - proc replaceById(id: cstring; newTree: Node) = let x = document.getElementById(id) x.parentNode.replaceChild(newTree, x) @@ -155,18 +164,19 @@ proc eq(a, b: VNode; deep: bool): EqResult = # Do not test event listeners here! return result -proc updateDirtyElements(parent, current: Node, newNode: VNode) = +proc updateDirtyElements(parent, current: Node, newNode: VNode, + kxi: KaraxInstance) = if newNode.key >= 0 and isDirty(newNode.key): unmarkDirty(newNode.key) - let n = vnodeToDom(newNode) + let n = vnodeToDom(newNode, kxi) if parent == nil: - replaceById("ROOT", n) + replaceById(kxi.rootId, n) else: parent.replaceChild(n, current) elif newNode.kind != VNodeKind.text and newNode.kind != VNodeKind.vthunk and newNode.kind != VNodeKind.dthunk: for i in 0..newNode.len-1: - updateDirtyElements(current, current[i], newNode[i]) + updateDirtyElements(current, current[i], newNode[i], kxi) # leave early if we know there cannot be anything left to do: #if dirtyCount <= 0: return @@ -187,13 +197,14 @@ proc updateDom(newNode, oldNode: VNode) = for i in 0 ..< newNode.len: updateDom(newNode[i], oldNode[i]) -proc updateElement(parent, current: Node, newNode, oldNode: VNode) = +proc updateElement(parent, current: Node, newNode, oldNode: VNode; + kxi: KaraxInstance) = let res = eq(newNode, oldNode, deep=false) if res == different: detach(oldNode) - let n = vnodeToDom(newNode) + let n = vnodeToDom(newNode, kxi) if parent == nil: - replaceById("ROOT", n) + replaceById(kxi.rootId, n) else: parent.replaceChild(n, current) else: @@ -207,7 +218,7 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) = assert oldNode.kind == newNode.kind when defined(simpleDiff): for i in 0..min(newLength, oldLength)-1: - updateElement(current, current[i], newNode[i], oldNode[i]) + updateElement(current, current[i], newNode[i], oldNode[i], kxi) if newLength > oldLength: for i in oldLength..newLength-1: current.appendChild(vnodeToDom(newNode[i])) @@ -243,14 +254,14 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) = var pos = min(oldPos, newPos) + 1 for i in commonPrefix..pos-1: - updateElement(current, current.childNodes[i], newNode[i], oldNode[i]) + updateElement(current, current.childNodes[i], newNode[i], oldNode[i], kxi) var nextChildPos = oldPos + 1 while pos <= newPos: if nextChildPos == oldLength: - current.appendChild(vnodeToDom(newNode[pos])) + current.appendChild(vnodeToDom(newNode[pos], kxi)) else: - current.insertBefore(vnodeToDom(newNode[pos]), current.childNodes[nextChildPos]) + current.insertBefore(vnodeToDom(newNode[pos], kxi), current.childNodes[nextChildPos]) # added new Node, so old state of VDOM have one more Node inc oldLength inc pos @@ -263,53 +274,57 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) = when false: var drawTimeout: Timeout -proc dodraw() = - if dorender.isNil: return - let newtree = dorender() - newtree.id = "ROOT" - toFocus = nil - if currentTree == nil: - currentTree = newtree - let asdom = vnodeToDom currentTree - replaceById("ROOT", asdom) +proc dodraw(kxi: KaraxInstance) = + if kxi.renderer.isNil: return + let newtree = kxi.renderer() + newtree.id = kxi.rootId + kxi.toFocus = nil + if kxi.currentTree == nil: + kxi.currentTree = newtree + let asdom = vnodeToDom(kxi.currentTree, kxi) + replaceById(kxi.rootId, asdom) else: - let olddom = document.getElementById("ROOT") - updateElement(nil, olddom, newtree, currentTree) + let olddom = document.getElementById(kxi.rootId) + updateElement(nil, olddom, newtree, kxi.currentTree, kxi) #assert same(newtree, document.getElementById("ROOT")) if someDirty: - updateDirtyElements(nil, olddom, newtree) + updateDirtyElements(nil, olddom, newtree, kxi) someDirty = false - currentTree = newtree + kxi.currentTree = newtree - if not postRenderCallback.isNil: - postRenderCallback() + if not kxi.postRenderCallback.isNil: + kxi.postRenderCallback() # now that it's part of the DOM, give it the focus: - if toFocus != nil: - toFocus.focus() + if kxi.toFocus != nil: + kxi.toFocus.focus() proc reqFrame(callback: proc()) {.importc: "window.requestAnimationFrame".} -proc redraw*() = +proc redraw*(kxi: KaraxInstance = kxi) = # we buffer redraw requests: when false: if drawTimeout != nil: clearTimeout(drawTimeout) drawTimeout = setTimeout(dodraw, 30) elif true: - reqFrame(dodraw) + reqFrame(proc () = kxi.dodraw) else: dodraw() proc init(ev: Event) = - reqFrame(dodraw) + reqFrame(proc () = kxi.dodraw) -proc setRenderer*(renderer: proc (): VNode, clientPostRenderCallback: proc () = nil) = - dorender = renderer +proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT", + clientPostRenderCallback: proc () = nil): KaraxInstance {.discardable.} = + ## Setup Karax. Usually the return value can be ignored. + result = KaraxInstance(rootId: root, renderer: renderer, + postRenderCallback: clientPostRenderCallback) + kxi = result window.onload = init - postRenderCallback = clientPostRenderCallback -proc addEventHandler*(n: VNode; k: EventKind; action: EventHandler) = +proc addEventHandler*(n: VNode; k: EventKind; action: EventHandler; + kxi: KaraxInstance = kxi) = ## Implements the foundation of Karax's event management. ## Karax DSL transforms ``tag(onEvent = handler)`` to ## ``tempNode.addEventHandler(tagNode, EventKind.onEvent, wrapper)`` @@ -317,7 +332,7 @@ proc addEventHandler*(n: VNode; k: EventKind; action: EventHandler) = ## a ``redraw``. proc wrapper(ev: Event; n: VNode) = action(ev, n) - redraw() + redraw(kxi) addEventListener(n, k, wrapper) proc setOnHashChange*(action: proc (hashPart: cstring)) = @@ -346,10 +361,10 @@ proc setupErrorHandler*() = proc prepend(parent, kid: Element) = parent.insertBefore(kid, parent.firstChild) -proc loadScript*(jsfilename: cstring) = +proc loadScript*(jsfilename: cstring; kxi: KaraxInstance = kxi) = let body = getElementById("body") let s = document.createElement("script") s.setAttr "type", "text/javascript" s.setAttr "src", jsfilename body.prepend(s) - redraw() + redraw(kxi) diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index 190545c..a3558c8 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -76,7 +76,7 @@ proc tcall2(n, tmpContext: NimNode): NimNode = let key = getName x[0] if key.startsWith("on"): result.add newCall(bindSym"addEventHandler", - tmp, newDotExpr(bindSym"EventKind", x[0]), x[1]) + tmp, newDotExpr(bindSym"EventKind", x[0]), x[1], ident("kxi")) elif key in SpecialAttrs: result.add newDotAsgn(tmp, key, x[1]) else: @@ -84,7 +84,7 @@ proc tcall2(n, tmpContext: NimNode): NimNode = elif ck != ComponentKind.Tag: call.add x elif eqIdent(x, "setFocus"): - result.add newCall(x, tmp) + result.add newCall(x, tmp, ident"kxi") else: result.add tcall2(x, tmp) if tmpContext == nil: From 28c4f4b76a0cf66618a70a9a4e9f6d9f220040a6 Mon Sep 17 00:00:00 2001 From: Araq Date: Fri, 16 Jun 2017 22:57:32 +0200 Subject: [PATCH 045/247] update kajax.nim to use the 'kxi' context too --- src/kajax.nim | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/kajax.nim b/src/kajax.nim index be230f0..414b9d2 100644 --- a/src/kajax.nim +++ b/src/kajax.nim @@ -7,10 +7,11 @@ import karax proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)]; data: cstring; - cont: proc (httpStatus: int; response: cstring)) = + cont: proc (httpStatus: int; response: cstring); + kxi: KaraxInstance = kxi) = proc contWrapper(httpStatus: int; response: cstring) = cont(httpStatus, response) - redraw() + redraw(kxi) type HttpRequest {.importc.} = ref object @@ -39,12 +40,14 @@ proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)]; proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)]; data: cstring; - cont: proc (httpStatus: int, response: cstring)) = - ajax("POST", url, headers, data, cont) + cont: proc (httpStatus: int, response: cstring); + kxi: KaraxInstance = kxi) = + ajax("POST", url, headers, data, cont, kxi) proc ajaxGet*(url: cstring; headers: openarray[(cstring, cstring)]; - cont: proc (httpStatus: int, response: cstring)) = - ajax("GET", url, headers, nil, cont) + cont: proc (httpStatus: int, response: cstring); + kxi: KaraxInstance = kxi) = + ajax("GET", url, headers, nil, cont, kxi) proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".} proc fromJson*[T](blob: cstring): T {.importc: "JSON.parse".} From 7b246d0d40612e230cd53bbbb535428711aff585 Mon Sep 17 00:00:00 2001 From: Fabian Keller Date: Sat, 17 Jun 2017 16:57:36 +0200 Subject: [PATCH 046/247] added travis link --- readme.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.rst b/readme.rst index 4aafac8..13e670a 100644 --- a/readme.rst +++ b/readme.rst @@ -1,4 +1,4 @@ -Karax -- Single page applications in Nim +Karax – Single page applications in Nim [![Build Status](https://travis-ci.org/pragmagic/karax.svg?branch=master)](https://travis-ci.org/pragmagic/karax) ======================================== Karax is a framework for developing single page applications in Nim. From 972fcb0950a275e06f3fab0b054d264cc386fdbd Mon Sep 17 00:00:00 2001 From: Fabian Keller Date: Sat, 17 Jun 2017 17:05:37 +0200 Subject: [PATCH 047/247] proper rst solution --- readme.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/readme.rst b/readme.rst index 13e670a..3fac7ad 100644 --- a/readme.rst +++ b/readme.rst @@ -1,5 +1,5 @@ -Karax – Single page applications in Nim [![Build Status](https://travis-ci.org/pragmagic/karax.svg?branch=master)](https://travis-ci.org/pragmagic/karax) -======================================== +Karax – Single page applications in Nim |travis| +================================================ Karax is a framework for developing single page applications in Nim. It's still in heavy development, so keep in mind that the API is subject @@ -33,3 +33,6 @@ 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 From b189b24a4f6f3afb5856db9634a55cbf6ca86375 Mon Sep 17 00:00:00 2001 From: Fabian Keller Date: Sun, 18 Jun 2017 00:01:27 +0200 Subject: [PATCH 048/247] fixes #25 (#26) --- .gitignore | 1 + src/karaxdsl.nim | 10 ++++++++-- tests/compiler_tests.nim | 18 ++++++++++++++++++ tests/{diffDomTests.nims => nim.cfg} | 0 tests/tester.nim | 1 + 5 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 tests/compiler_tests.nim rename tests/{diffDomTests.nims => nim.cfg} (100%) diff --git a/.gitignore b/.gitignore index 67d9b34..42b07a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ nimcache/ +/tests/tester \ No newline at end of file diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index a3558c8..e3e8290 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -47,12 +47,18 @@ proc tcall2(n, tmpContext: NimNode): NimNode = assert n.len == result.len if L > 0: result[L-1] = tcall2(result[L-1], tmpContext) - of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkCaseStmt, - nnkTryStmt, nnkFinally: + of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkTryStmt, + nnkFinally: # recurse for every child: result = copyNimNode(n) for x in n: result.add tcall2(x, tmpContext) + of nnkCaseStmt: + # recurse for children, but don't add call for case ident + result = copyNimNode(n) + result.add n[0] + for i in 1 ..< n.len: + result.add tcall2(n[i], tmpContext) of nnkVarSection, nnkLetSection, nnkConstSection: result = n of nnkCallKinds: diff --git a/tests/compiler_tests.nim b/tests/compiler_tests.nim new file mode 100644 index 0000000..4352cab --- /dev/null +++ b/tests/compiler_tests.nim @@ -0,0 +1,18 @@ +include karaxprelude + +block: + type + TestEnum {.pure.} = enum + ValA, ValB + + var e = TestEnum.ValA + + proc renderDom(): VNode {.used.} = + result = buildHtml(): + tdiv(): + case e + of TestEnum.ValA: + tdiv: text "A" + of TestEnum.ValB: + tdiv: text "B" + diff --git a/tests/diffDomTests.nims b/tests/nim.cfg similarity index 100% rename from tests/diffDomTests.nims rename to tests/nim.cfg diff --git a/tests/tester.nim b/tests/tester.nim index 5dc7426..482cad3 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -8,6 +8,7 @@ proc exec(cmd: string) = proc main = exec("nim js tests/diffDomTests.nim") + exec("nim js tests/compiler_tests.nim") exec("nim js examples/todoapp/todoapp.nim") exec("nim js examples/scrollapp/scrollapp.nim") exec("nim js examples/mediaplayer/playerapp.nim") From d1e0e1367b417114ddd5ac44b149c069c43eb389 Mon Sep 17 00:00:00 2001 From: Araq Date: Sun, 18 Jun 2017 18:01:48 +0200 Subject: [PATCH 049/247] wip --- experiments/example.nim | 79 +++++++++++++++++++++++---------------- experiments/refs/demo.nim | 18 +++++---- src/karax.nim | 4 ++ src/vdom.nim | 11 +++--- 4 files changed, 66 insertions(+), 46 deletions(-) diff --git a/experiments/example.nim b/experiments/example.nim index fd1e208..d48143d 100644 --- a/experiments/example.nim +++ b/experiments/example.nim @@ -1,57 +1,72 @@ -import vdom, vstyles, components, karax, karaxdsl, jdict, jstrutils, kdom +import vdom, vstyles, karax, karaxdsl, jdict, jstrutils, kdom + +type + Car = ref object of VComponent + counter: int + cntdown: int + timer: TimeOut + list: seq[cstring] + change: bool + onclick: proc (ev: Event; n: VNode) + +const ticksUntilChange = 5 + +proc car*(images: seq[cstring]): Car = + new(result) + result.list = images + result.cntdown = ticksUntilChange + + proc docount() = + dec result.cntdown + if result.cntdown == 0: + result.counter = (result.counter + 1) mod result.list.len + result.cntdown = ticksUntilChange + else: + result.timer = setTimeout(docount, 30) + result.change = true + redraw() + + result.onclick = proc (ev: Event; n: VNode) = + result.change = false + if result.timer != nil: + clearTimeout(result.timer) + result.timer = setTimeout(docount, 30) + +method changed(c: Car): bool = true var images: seq[cstring] = @[cstring"a", "b", "c", "d"] -proc carousel*(key: VKey): VNode {.component.} = - state: - var counter: int = 0 - var cntdown: int = 5 - var timer: Timeout = nil - - proc docount() = - cntdown = cntdown - 1 - if cntdown == 0: - counter = (counter + 1) mod images.len - cntdown = 5 - else: - timer = setTimeout(docount, 30) - redraw() - - proc next(ev: Event; n: VNode) = - if timer != nil: - clearTimeout(timer) - timer = setTimeout(docount, 30) - +proc carousel*(): VNode = + let c = car(images) let col = - case counter + case c.counter of 0: cstring"#4d4d4d" of 1: cstring"#ff00ff" of 2: cstring"#00ffff" of 3: cstring"#ffff00" else: cstring"red" - - result = buildHtml(tdiv(key=key)): - text images[counter] - button(onclick = next): + result = buildHtml(tdiv(nref = c)): + text images[c.counter] + button(onclick = c.onclick): text "Next" tdiv(style = style(StyleAttr.color, col)): text "This changes its color." - if cntdown != 5: - text &cntdown + if c.cntdown != ticksUntilChange: + text &c.cntdown proc createDom(): VNode = result = buildHtml(table): tr: td: - carousel(0) + carousel() td: - carousel(1) + carousel() tr: td: - carousel(2) + carousel() td: - carousel(3) + carousel() setRenderer createDom diff --git a/experiments/refs/demo.nim b/experiments/refs/demo.nim index 6ecc7db..a0db59f 100644 --- a/experiments/refs/demo.nim +++ b/experiments/refs/demo.nim @@ -2,7 +2,7 @@ include karaxprelude import future, sequtils type - CustomRef = ref object of VNodeRef + CustomRef = ref object of VComponent method onAttach(r: CustomRef) = kout(cstring"custom ref attached") @@ -13,10 +13,10 @@ method onDetach(r: CustomRef) = var modelData = @[5, 2, 4] - refA = VNodeRef() - refB = VNodeRef() + refA = VComponent() + refB = VComponent() refC = CustomRef() - refSeq = newSeq[VNodeRef]() + refSeq = newSeq[VComponent]() proc onClick(ev: Event, n: VNode) = modelData.add(0) @@ -25,11 +25,13 @@ proc showRefs() = kout(refA) kout(refB) kout(refC) - kout(refSeq.map(nref => nref.vnode)) + for i in 0 ..< refSeq.len: + kout(refSeq[i].vnode) + #kout(refSeq.map(nref => nref.vnode)) -proc secureRefSlot(i: int): VNodeRef = +proc secureRefSlot(i: int): VComponent = while refSeq.len <= i: - refSeq.add(VNodeRef()) + refSeq.add(VComponent()) result = refSeq[i] proc view(): VNode = @@ -53,4 +55,4 @@ proc view(): VNode = proc renderer(): VNode = view() -setRenderer(renderer, showRefs) +setRenderer(renderer, "ROOT", showRefs) diff --git a/src/karax.nim b/src/karax.nim index 77cebdf..56357ef 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -150,6 +150,9 @@ proc eq(a, b: VNode; deep: bool): EqResult = if a.id != b.id: return different result = identical if a.key != b.key: return different + if a.nref != nil and changed(a.nref): + kout cstring"yes, change!" + return different if a.kind == VNodeKind.text: if a.text != b.text: return different elif a.kind == VNodeKind.vthunk or a.kind == VNodeKind.dthunk: @@ -201,6 +204,7 @@ proc updateStyles(newNode, oldNode: VNode; deep: bool) = proc updateDom(newNode, oldNode: VNode) = newNode.dom = oldNode.dom + newNode.nref = oldNode.nref assert newNode.len == oldNode.len for i in 0 ..< newNode.len: updateDom(newNode[i], oldNode[i]) diff --git a/src/vdom.nim b/src/vdom.nim index 9f9c642..e478da1 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -101,8 +101,7 @@ macro buildLookupTables(): untyped = buildLookupTables() type - VNodeRef* = ref object of RootObj - vnode*: VNode + VComponent* = ref object of RootObj EventHandler* = proc (ev: Event; target: VNode) {.closure.} VKey* = int @@ -111,7 +110,7 @@ type kind*: VNodeKind key*: VKey id*, class*, text*: cstring - nref*: VNodeRef + nref*: VComponent kids: seq[VNode] # even index: key, odd index: value; done this way for memory efficiency: attrs: seq[cstring] @@ -122,9 +121,9 @@ type dom*: Node ## the attached real DOM node. Can be 'nil' if the virtual node ## is not part of the virtual DOM anymore. -method onAttach*(v: VNodeRef) {.base.} = discard -method onDetach*(v: VNodeRef) {.base.} = discard -method changed*(v: VNodeRef): bool {.base.} = false +method onAttach*(v: VComponent) {.base.} = discard +method onDetach*(v: VComponent) {.base.} = discard +method changed*(v: VComponent): bool {.base.} = false proc value*(n: VNode): cstring = n.text proc `value=`*(n: VNode; v: cstring) = n.text = v From 4801b8d8c115fec6301a4307cd18a791290fedcc Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 19 Jun 2017 21:25:34 +0200 Subject: [PATCH 050/247] new component example works --- experiments/example.nim | 79 +++++++++++++++++++++++------------------ src/karax.nim | 61 ++++++++++++++++++++----------- src/karaxdsl.nim | 2 +- src/vdom.nim | 22 ++++++++---- 4 files changed, 100 insertions(+), 64 deletions(-) diff --git a/experiments/example.nim b/experiments/example.nim index d48143d..5653e22 100644 --- a/experiments/example.nim +++ b/experiments/example.nim @@ -2,59 +2,68 @@ import vdom, vstyles, karax, karaxdsl, jdict, jstrutils, kdom type - Car = ref object of VComponent + Carousel = ref object of VComponent counter: int - cntdown: int + cntdown, myid: int timer: TimeOut list: seq[cstring] change: bool - onclick: proc (ev: Event; n: VNode) const ticksUntilChange = 5 -proc car*(images: seq[cstring]): Car = - new(result) - result.list = images - result.cntdown = ticksUntilChange - - proc docount() = - dec result.cntdown - if result.cntdown == 0: - result.counter = (result.counter + 1) mod result.list.len - result.cntdown = ticksUntilChange - else: - result.timer = setTimeout(docount, 30) - result.change = true - redraw() - - result.onclick = proc (ev: Event; n: VNode) = - result.change = false - if result.timer != nil: - clearTimeout(result.timer) - result.timer = setTimeout(docount, 30) - -method changed(c: Car): bool = true - var images: seq[cstring] = @[cstring"a", "b", "c", "d"] -proc carousel*(): VNode = - let c = car(images) +proc render(x: VComponent): VNode = + let self = Carousel(x) + + proc docount() = + dec self.cntdown + if self.cntdown == 0: + self.counter = (self.counter + 1) mod self.list.len + self.cntdown = ticksUntilChange + else: + self.timer = setTimeout(docount, 30) + self.change = true + redraw() + + proc onclick(ev: Event; n: VNode) = + self.change = false + if self.timer != nil: + clearTimeout(self.timer) + self.timer = setTimeout(docount, 30) + let col = - case c.counter + case self.counter of 0: cstring"#4d4d4d" of 1: cstring"#ff00ff" of 2: cstring"#00ffff" of 3: cstring"#ffff00" else: cstring"red" - result = buildHtml(tdiv(nref = c)): - text images[c.counter] - button(onclick = c.onclick): + result = buildHtml(tdiv()): + text self.list[self.counter] + button(onclick = onclick): text "Next" tdiv(style = style(StyleAttr.color, col)): - text "This changes its color." - if c.cntdown != ticksUntilChange: - text &c.cntdown + text "This changes its color." & &self.myid + if self.cntdown != ticksUntilChange: + text &self.cntdown + +var gid: int + +proc carousel(): Carousel = + result = Carousel(kind: VNodeKind.component, key: -1) + result.render = render + result.list = images + result.changed = proc (c: VComponent): bool = + let x = Carousel(c) + result = x.change + result.cntdown = ticksUntilChange + result.myid = gid + inc gid + #result.onAttach = proc (_: VComponent) = + # result.cntdown = ticksUntilChange + # kout cstring"attached!" proc createDom(): VNode = result = buildHtml(table): diff --git a/src/karax.nim b/src/karax.nim index 56357ef..f15326d 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -80,14 +80,11 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind; action: EventHandler) = # --------------------- DOM diff ----------------------------------------- template detach(n: VNode) = - if not n.nref.isNil(): - n.nref.vnode = nil - n.nref.onDetach() + if n.kind == VNodeKind.component: + let x = VComponent(n) + if x.onDetach != nil: x.onDetach(x) n.dom = nil template attach(n: VNode) = - if not n.nref.isNil(): - n.nref.vnode = n - n.nref.onAttach() n.dom = result proc vnodeToDom(n: VNode; kxi: KaraxInstance): Node = @@ -105,6 +102,13 @@ proc vnodeToDom(n: VNode; kxi: KaraxInstance): Node = #n.key = result.key attach n return result + elif n.kind == VNodeKind.component: + let x = VComponent(n) + if x.onAttach != nil: x.onAttach(x) + assert x.render != nil + result = vnodeToDom(x.render(x), kxi) + attach n + return result else: result = document.createElement(toTag[n.kind]) attach n @@ -150,9 +154,6 @@ proc eq(a, b: VNode; deep: bool): EqResult = if a.id != b.id: return different result = identical if a.key != b.key: return different - if a.nref != nil and changed(a.nref): - kout cstring"yes, change!" - return different if a.kind == VNodeKind.text: if a.text != b.text: return different elif a.kind == VNodeKind.vthunk or a.kind == VNodeKind.dthunk: @@ -160,6 +161,11 @@ proc eq(a, b: VNode; deep: bool): EqResult = if a.len != b.len: return different for i in 0..= commonPrefix and newPos >= commonPrefix: - eqAndUpdate(newNode[newPos], oldNode[oldPos]): + eqAndUpdate(newNode, newPos, oldNode[oldPos]): dec oldPos dec newPos var pos = min(oldPos, newPos) + 1 for i in commonPrefix..pos-1: - updateElement(current, current.childNodes[i], newNode[i], oldNode[i], kxi) + if updateElement(current, current.childNodes[i], newNode[i], oldNode[i], kxi): + newNode[i] = oldNode[i] var nextChildPos = oldPos + 1 while pos <= newPos: @@ -297,7 +316,7 @@ proc dodraw(kxi: KaraxInstance) = replaceById(kxi.rootId, asdom) else: let olddom = document.getElementById(kxi.rootId) - updateElement(nil, olddom, newtree, kxi.currentTree, kxi) + discard updateElement(nil, olddom, newtree, kxi.currentTree, kxi) #assert same(newtree, document.getElementById("ROOT")) if someDirty: updateDirtyElements(nil, olddom, newtree, kxi) @@ -363,7 +382,7 @@ proc setupErrorHandler*() = ## the error message is put into the console, otherwise `alert` ## is called. proc stackTraceAsCstring(): cstring = cstring(getStackTrace()) - var onerror {.importc: "window.onerror".} = + var onerror {.importc: "window.onerror", used.} = proc (msg, url: cstring, line, col: int, error: cstring): bool = var x = cstring"Error: " & msg & "\n" & stackTraceAsCstring() kout(x) diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index 07a336f..a3558c8 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -4,7 +4,7 @@ from strutils import startsWith, toLowerAscii const StmtContext = ["kout", "inc", "echo", "dec", "!"] - SpecialAttrs = ["id", "class", "value", "key", "style", "nref"] + SpecialAttrs = ["id", "class", "value", "key", "style"] proc getName(n: NimNode): string = case n.kind diff --git a/src/vdom.nim b/src/vdom.nim index e478da1..f25b1a7 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -8,6 +8,7 @@ type VNodeKind* {.pure.} = enum text = "#text", int = "#int", bool = "#bool", vthunk = "#vthunk", dthunk = "#dthunk", + component = "#component", html, head, title, base, link, meta, style, script, noscript, @@ -101,16 +102,13 @@ macro buildLookupTables(): untyped = buildLookupTables() type - VComponent* = ref object of RootObj - EventHandler* = proc (ev: Event; target: VNode) {.closure.} VKey* = int - VNode* = ref object + VNode* = ref object of RootObj kind*: VNodeKind key*: VKey id*, class*, text*: cstring - nref*: VComponent kids: seq[VNode] # even index: key, odd index: value; done this way for memory efficiency: attrs: seq[cstring] @@ -121,9 +119,11 @@ type dom*: Node ## the attached real DOM node. Can be 'nil' if the virtual node ## is not part of the virtual DOM anymore. -method onAttach*(v: VComponent) {.base.} = discard -method onDetach*(v: VComponent) {.base.} = discard -method changed*(v: VComponent): bool {.base.} = false + VComponent* = ref object of VNode + render*: proc(self: VComponent): VNode + changed*: proc(self: VComponent): bool + onAttach*: proc(self: VComponent) + onDetach*: proc(self: VComponent) proc value*(n: VNode): cstring = n.text proc `value=`*(n: VNode; v: cstring) = n.text = v @@ -143,6 +143,13 @@ proc vthunk*(name: cstring; args: varargs[VNode, vn]): VNode = proc dthunk*(name: cstring; args: varargs[VNode, vn]): VNode = VNode(kind: VNodeKind.dthunk, text: name, key: -1, kids: @args) +proc vcomponent*(render: (proc(self: VComponent): VNode) not nil, + changed: (proc(self: VComponent): bool) not nil, + onAttach: proc(self: VComponent) = nil, + onDetach: proc(self: VComponent) = nil): VNode = + result = VComponent(kind: VNodeKind.component, key: -1, render: render, + changed: changed, onAttach: onAttach, onDetach: onDetach) + proc setAttr*(n: VNode; key: cstring; val: cstring = "") = if n.attrs.isNil: n.attrs = @[key, val] @@ -160,6 +167,7 @@ proc getAttr*(n: VNode; key: cstring): cstring = proc len*(x: VNode): int = x.kids.len proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx] +proc `[]=`*(x: VNode; idx: int; y: VNode) = x.kids[idx] = y proc add*(parent, kid: VNode) = parent.kids.add kid proc newVNode*(kind: VNodeKind): VNode = VNode(kind: kind, key: -1) From c86916e37df24647cee1f4b24f565cd539e4a02a Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Tue, 20 Jun 2017 00:14:54 +0200 Subject: [PATCH 051/247] stateful components now in a workable state; renamed .component macro to .compact which much better captures its meaning --- examples/mediaplayer/mediaplayer.nim | 4 +- examples/todoapp/todoapp.nim | 8 +- experiments/example.nim | 10 +- experiments/scrollapp.nim | 2 +- src/compact.nim | 121 ++++++++++++++ src/components.nim | 225 --------------------------- src/karax.nim | 83 +++++----- src/karaxdsl.nim | 2 +- src/karaxprelude.nim | 2 +- src/vdom.nim | 19 ++- 10 files changed, 185 insertions(+), 291 deletions(-) create mode 100644 src/compact.nim delete mode 100644 src/components.nim diff --git a/examples/mediaplayer/mediaplayer.nim b/examples/mediaplayer/mediaplayer.nim index 890c078..3f970a8 100644 --- a/examples/mediaplayer/mediaplayer.nim +++ b/examples/mediaplayer/mediaplayer.nim @@ -1,5 +1,5 @@ -import karax, karaxdsl, vdom, kdom, components +import karax, karaxdsl, vdom, kdom, compact const Play = 0 @@ -12,7 +12,7 @@ proc play(n: Node) {.importcpp.} proc pause(n: Node) {.importcpp.} proc `width=`(n: Node, w: int) {.importcpp: "#.width = #".} -proc mplayer*(id, resource: cstring): VNode {.component.} = +proc mplayer*(id, resource: cstring): VNode {.compact.} = proc handler(ev: Event; n: VNode) = let myVideo = document.getElementById(id) case n.key diff --git a/examples/todoapp/todoapp.nim b/examples/todoapp/todoapp.nim index 802bfc9..f74abc3 100644 --- a/examples/todoapp/todoapp.nim +++ b/examples/todoapp/todoapp.nim @@ -1,5 +1,5 @@ -import vdom, karax, karaxdsl, jstrutils, components, localstorage +import vdom, karax, karaxdsl, jstrutils, compact, localstorage type Filter = enum @@ -77,7 +77,7 @@ proc toChecked(checked: bool): cstring = proc selected(v: Filter): cstring = (if filter == v: cstring"selected" else: cstring(nil)) -proc createEntry(id: int; d: cstring; completed, selected: bool): VNode {.component.} = +proc createEntry(id: int; d: cstring; completed, selected: bool): VNode {.compact.} = result = buildHtml(tr): li(class=toClass(completed)): if not selected: @@ -92,7 +92,7 @@ proc createEntry(id: int; d: cstring; completed, selected: bool): VNode {.compon onblur = focusLost, onkeyupenter = editEntry, value = d, setFocus) -proc makeFooter(entriesCount, completedCount: int): VNode {.component.} = +proc makeFooter(entriesCount, completedCount: int): VNode {.compact.} = result = buildHtml(footer(class = "footer")): span(class = "todo-count"): strong: @@ -111,7 +111,7 @@ proc makeFooter(entriesCount, completedCount: int): VNode {.component.} = button(class = "clear-completed", onclick = clearCompleted): text "Clear completed (" & &completedCount & ")" -proc makeHeader(): VNode {.component.} = +proc makeHeader(): VNode {.compact.} = result = buildHtml(header(class = "header")): h1: text "todos" diff --git a/experiments/example.nim b/experiments/example.nim index 5653e22..fa1e739 100644 --- a/experiments/example.nim +++ b/experiments/example.nim @@ -51,13 +51,13 @@ proc render(x: VComponent): VNode = var gid: int +proc changed(c: VComponent): bool = + let x = Carousel(c) + result = x.change + proc carousel(): Carousel = - result = Carousel(kind: VNodeKind.component, key: -1) - result.render = render + result = newComponent(Carousel, render, changed) result.list = images - result.changed = proc (c: VComponent): bool = - let x = Carousel(c) - result = x.change result.cntdown = ticksUntilChange result.myid = gid inc gid diff --git a/experiments/scrollapp.nim b/experiments/scrollapp.nim index f9c8ccb..86a802f 100644 --- a/experiments/scrollapp.nim +++ b/experiments/scrollapp.nim @@ -1,5 +1,5 @@ -import vdom, karax, karaxdsl, jdict, jstrutils, components +import vdom, karax, karaxdsl, jdict, jstrutils, compact type Filter = enum diff --git a/src/compact.nim b/src/compact.nim new file mode 100644 index 0000000..51de228 --- /dev/null +++ b/src/compact.nim @@ -0,0 +1,121 @@ +## 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.. = 0 and isDirty(newNode.key): - unmarkDirty(newNode.key) - let n = vnodeToDom(newNode, kxi) - if parent == nil: - replaceById(kxi.rootId, n) - else: - parent.replaceChild(n, current) - elif newNode.kind != VNodeKind.text and newNode.kind != VNodeKind.vthunk and - newNode.kind != VNodeKind.dthunk: - for i in 0..newNode.len-1: - updateDirtyElements(current, current[i], newNode[i], kxi) - # leave early if we know there cannot be anything left to do: - #if dirtyCount <= 0: return +when false: + proc updateDirtyElements(parent, current: Node, newNode: VNode, + kxi: KaraxInstance) = + if newNode.key >= 0 and isDirty(newNode.key): + unmarkDirty(newNode.key) + let n = vnodeToDom(newNode, kxi) + if parent == nil: + replaceById(kxi.rootId, n) + else: + parent.replaceChild(n, current) + elif newNode.kind != VNodeKind.text and newNode.kind != VNodeKind.vthunk and + newNode.kind != VNodeKind.dthunk: + for i in 0..newNode.len-1: + updateDirtyElements(current, current[i], newNode[i], kxi) + # leave early if we know there cannot be anything left to do: + #if dirtyCount <= 0: return proc updateStyles(newNode, oldNode: VNode; deep: bool) = # we keep the oldNode, but take over the style from the new node: @@ -216,21 +218,17 @@ proc updateDom(newNode, oldNode: VNode) = proc updateElement(parent, current: Node, newNode, oldNode: VNode; kxi: KaraxInstance): bool = - if oldNode.kind == newNode.kind and oldNode.kind == VNodeKind.component: - let x = VComponent(oldNode) - assert x.changed != nil - if x.changed(x): - let n = vnodeToDom(x.render(x), kxi) - if parent == nil: - replaceById(kxi.rootId, n) - else: - parent.replaceChild(n, current) - return true - let res = eq(newNode, oldNode, deep=false) - if res == different: - detach(oldNode) - let n = vnodeToDom(newNode, kxi) + if res <= different: + var n: Node + if res == changed: + assert oldNode.kind == VNodeKind.component + let x = VComponent(oldNode) + n = vnodeToDom(x.renderImpl(x), kxi) + result = true + else: + detach(oldNode) + n = vnodeToDom(newNode, kxi) if parent == nil: replaceById(kxi.rootId, n) else: @@ -264,7 +262,7 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; a[i] = b #updateDom(a, b) action - of different: break + of different, changed: break of similar: #updateDom(a, b) a[i] = b @@ -318,9 +316,6 @@ proc dodraw(kxi: KaraxInstance) = let olddom = document.getElementById(kxi.rootId) discard updateElement(nil, olddom, newtree, kxi.currentTree, kxi) #assert same(newtree, document.getElementById("ROOT")) - if someDirty: - updateDirtyElements(nil, olddom, newtree, kxi) - someDirty = false kxi.currentTree = newtree if not kxi.postRenderCallback.isNil: diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index a3558c8..e46ee62 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -1,5 +1,5 @@ -import macros, karax, vdom, components +import macros, karax, vdom, compact from strutils import startsWith, toLowerAscii const diff --git a/src/karaxprelude.nim b/src/karaxprelude.nim index 285730a..ed9349c 100644 --- a/src/karaxprelude.nim +++ b/src/karaxprelude.nim @@ -1,3 +1,3 @@ ## Include file that contains the common imports for the Karax framework. -import karax, karaxdsl, vdom, components +import karax, karaxdsl, vdom, compact diff --git a/src/vdom.nim b/src/vdom.nim index f25b1a7..8c19ea2 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -120,10 +120,10 @@ type ## is not part of the virtual DOM anymore. VComponent* = ref object of VNode - render*: proc(self: VComponent): VNode - changed*: proc(self: VComponent): bool - onAttach*: proc(self: VComponent) - onDetach*: proc(self: VComponent) + renderImpl*: proc(self: VComponent): VNode + changedImpl*: proc(self: VComponent): bool + onAttachImpl*: proc(self: VComponent) + onDetachImpl*: proc(self: VComponent) proc value*(n: VNode): cstring = n.text proc `value=`*(n: VNode; v: cstring) = n.text = v @@ -143,12 +143,15 @@ proc vthunk*(name: cstring; args: varargs[VNode, vn]): VNode = proc dthunk*(name: cstring; args: varargs[VNode, vn]): VNode = VNode(kind: VNodeKind.dthunk, text: name, key: -1, kids: @args) -proc vcomponent*(render: (proc(self: VComponent): VNode) not nil, +template newComponent*[T](t: typeDesc[T]; + render: (proc(self: VComponent): VNode) not nil, changed: (proc(self: VComponent): bool) not nil, onAttach: proc(self: VComponent) = nil, - onDetach: proc(self: VComponent) = nil): VNode = - result = VComponent(kind: VNodeKind.component, key: -1, render: render, - changed: changed, onAttach: onAttach, onDetach: onDetach) + onDetach: proc(self: VComponent) = nil): T = + ## Use this template to create new components. + T(kind: VNodeKind.component, key: -1, + text: cstring(astToStr(t)), renderImpl: render, + changedImpl: changed, onAttachImpl: onAttach, onDetachImpl: onDetach) proc setAttr*(n: VNode; key: cstring; val: cstring = "") = if n.attrs.isNil: From e58cbb78d20c94490a86337b5316a69ce0831884 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 21 Jun 2017 08:21:05 +0200 Subject: [PATCH 052/247] fixes jstrutils.isInt --- src/jstrutils.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jstrutils.nim b/src/jstrutils.nim index 0be4076..f0f22c0 100644 --- a/src/jstrutils.nim +++ b/src/jstrutils.nim @@ -19,7 +19,7 @@ proc `&`*(s: cstring): cstring {.importcpp: "(#)", nodecl.} proc isInt*(s: cstring): bool {.asmNoStackFrame.} = asm """ - return s.match(/^[0-9]+$/); + return `s`.match(/^[0-9]+$/); """ proc parseInt*(s: cstring): int {.importcpp: "parseInt(#, 10)", nodecl.} From d9648be60c21da7536359935f4daa802e8a3393d Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 21 Jun 2017 17:20:31 +0200 Subject: [PATCH 053/247] sane markDirty() mechanism for components --- experiments/carousel.html | 15 ++++++++++++ experiments/{example.nim => carousel.nim} | 11 +++------ experiments/{example.nims => carousel.nims} | 0 experiments/example.html | 15 ------------ src/karax.nim | 1 + src/vdom.nim | 26 +++++++++++++++++---- 6 files changed, 41 insertions(+), 27 deletions(-) create mode 100644 experiments/carousel.html rename experiments/{example.nim => carousel.nim} (89%) rename experiments/{example.nims => carousel.nims} (100%) delete mode 100644 experiments/example.html diff --git a/experiments/carousel.html b/experiments/carousel.html new file mode 100644 index 0000000..979dd38 --- /dev/null +++ b/experiments/carousel.html @@ -0,0 +1,15 @@ + + + + + Carousel app + + + + +
+ + + + + diff --git a/experiments/example.nim b/experiments/carousel.nim similarity index 89% rename from experiments/example.nim rename to experiments/carousel.nim index fa1e739..a820be7 100644 --- a/experiments/example.nim +++ b/experiments/carousel.nim @@ -1,3 +1,4 @@ +## This demo shows how you can develop your own stateful components with Karax. import vdom, vstyles, karax, karaxdsl, jdict, jstrutils, kdom @@ -7,7 +8,6 @@ type cntdown, myid: int timer: TimeOut list: seq[cstring] - change: bool const ticksUntilChange = 5 @@ -24,11 +24,10 @@ proc render(x: VComponent): VNode = self.cntdown = ticksUntilChange else: self.timer = setTimeout(docount, 30) - self.change = true + markDirty(self) redraw() proc onclick(ev: Event; n: VNode) = - self.change = false if self.timer != nil: clearTimeout(self.timer) self.timer = setTimeout(docount, 30) @@ -51,12 +50,8 @@ proc render(x: VComponent): VNode = var gid: int -proc changed(c: VComponent): bool = - let x = Carousel(c) - result = x.change - proc carousel(): Carousel = - result = newComponent(Carousel, render, changed) + result = newComponent(Carousel, render) result.list = images result.cntdown = ticksUntilChange result.myid = gid diff --git a/experiments/example.nims b/experiments/carousel.nims similarity index 100% rename from experiments/example.nims rename to experiments/carousel.nims diff --git a/experiments/example.html b/experiments/example.html deleted file mode 100644 index 2303d95..0000000 --- a/experiments/example.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - Todo app - - - - -
- - - - - diff --git a/src/karax.nim b/src/karax.nim index 42be93f..57c5162 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -225,6 +225,7 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; assert oldNode.kind == VNodeKind.component let x = VComponent(oldNode) n = vnodeToDom(x.renderImpl(x), kxi) + x.updatedImpl(x) result = true else: detach(oldNode) diff --git a/src/vdom.nim b/src/vdom.nim index 8c19ea2..4edcd4e 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -119,11 +119,16 @@ type dom*: Node ## the attached real DOM node. Can be 'nil' if the virtual node ## is not part of the virtual DOM anymore. - VComponent* = ref object of VNode + VComponent* = ref object of VNode ## The abstract class for every karax component. renderImpl*: proc(self: VComponent): VNode changedImpl*: proc(self: VComponent): bool + updatedImpl*: proc(self: VComponent) onAttachImpl*: proc(self: VComponent) onDetachImpl*: proc(self: VComponent) + version*: int ## Update this to trigger a redraw by karax. Usually you + ## should call 'markDirty' instead which is an alias for + ## 'inc version'. + renderedVersion*: int ## Do not touch. Used by karax. proc value*(n: VNode): cstring = n.text proc `value=`*(n: VNode; v: cstring) = n.text = v @@ -143,15 +148,28 @@ proc vthunk*(name: cstring; args: varargs[VNode, vn]): VNode = proc dthunk*(name: cstring; args: varargs[VNode, vn]): VNode = VNode(kind: VNodeKind.dthunk, text: name, key: -1, kids: @args) +proc defaultChangedImpl*(v: VComponent): bool = + ## The default implementation of 'changed'. + result = v.version != v.renderedVersion + +proc defaultUpdatedImpl*(v: VComponent) = + v.renderedVersion = v.version + template newComponent*[T](t: typeDesc[T]; render: (proc(self: VComponent): VNode) not nil, - changed: (proc(self: VComponent): bool) not nil, onAttach: proc(self: VComponent) = nil, - onDetach: proc(self: VComponent) = nil): T = + onDetach: proc(self: VComponent) = nil, + changed: (proc(self: VComponent): bool) = defaultChangedImpl, + updated: proc(self: VComponent) = defaultUpdatedImpl): T = ## Use this template to create new components. T(kind: VNodeKind.component, key: -1, text: cstring(astToStr(t)), renderImpl: render, - changedImpl: changed, onAttachImpl: onAttach, onDetachImpl: onDetach) + changedImpl: changed, updatedImpl: updated, + onAttachImpl: onAttach, onDetachImpl: onDetach) + +template markDirty*(c: VComponent) = + ## mark the component as dirty so that it is re-rendered. + inc c.version proc setAttr*(n: VNode; key: cstring; val: cstring = "") = if n.attrs.isNil: From a535ad709ba566767287b5af2a92613c30b14517 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 21 Jun 2017 17:57:02 +0200 Subject: [PATCH 054/247] carousel is now a real example --- {experiments => examples/carousel}/carousel.html | 0 {experiments => examples/carousel}/carousel.nim | 11 ++--------- examples/carousel/carousel.nims | 1 + experiments/carousel.nims | 1 - tests/tester.nim | 1 + 5 files changed, 4 insertions(+), 10 deletions(-) rename {experiments => examples/carousel}/carousel.html (100%) rename {experiments => examples/carousel}/carousel.nim (86%) create mode 100644 examples/carousel/carousel.nims delete mode 100644 experiments/carousel.nims diff --git a/experiments/carousel.html b/examples/carousel/carousel.html similarity index 100% rename from experiments/carousel.html rename to examples/carousel/carousel.html diff --git a/experiments/carousel.nim b/examples/carousel/carousel.nim similarity index 86% rename from experiments/carousel.nim rename to examples/carousel/carousel.nim index a820be7..c2db56c 100644 --- a/experiments/carousel.nim +++ b/examples/carousel/carousel.nim @@ -5,7 +5,7 @@ import vdom, vstyles, karax, karaxdsl, jdict, jstrutils, kdom type Carousel = ref object of VComponent counter: int - cntdown, myid: int + cntdown: int timer: TimeOut list: seq[cstring] @@ -44,21 +44,14 @@ proc render(x: VComponent): VNode = button(onclick = onclick): text "Next" tdiv(style = style(StyleAttr.color, col)): - text "This changes its color." & &self.myid + text "This changes its color." if self.cntdown != ticksUntilChange: text &self.cntdown -var gid: int - proc carousel(): Carousel = result = newComponent(Carousel, render) result.list = images result.cntdown = ticksUntilChange - result.myid = gid - inc gid - #result.onAttach = proc (_: VComponent) = - # result.cntdown = ticksUntilChange - # kout cstring"attached!" proc createDom(): VNode = result = buildHtml(table): diff --git a/examples/carousel/carousel.nims b/examples/carousel/carousel.nims new file mode 100644 index 0000000..1451fc0 --- /dev/null +++ b/examples/carousel/carousel.nims @@ -0,0 +1 @@ +--path: "../../src" diff --git a/experiments/carousel.nims b/experiments/carousel.nims deleted file mode 100644 index 6b5771c..0000000 --- a/experiments/carousel.nims +++ /dev/null @@ -1 +0,0 @@ ---path: "../src" diff --git a/tests/tester.nim b/tests/tester.nim index 5dc7426..e2d7cfd 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -11,5 +11,6 @@ proc main = exec("nim js examples/todoapp/todoapp.nim") exec("nim js examples/scrollapp/scrollapp.nim") exec("nim js examples/mediaplayer/playerapp.nim") + exec("nim js examples/carousel/carousel.nim") main() From 8dbc1da708a7e48433f335e4b76e02a30e47e638 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 21 Jun 2017 18:20:57 +0200 Subject: [PATCH 055/247] fixes #28 --- src/jstrutils.nim | 1 + src/vstyles.nim | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/src/jstrutils.nim b/src/jstrutils.nim index 63a9ae8..507e7a6 100644 --- a/src/jstrutils.nim +++ b/src/jstrutils.nim @@ -14,6 +14,7 @@ proc `&`*(a, b: cstring): cstring {.importcpp: "(# + #)", nodecl.} proc toCstr*(s: int): cstring {.importcpp: "((#)+'')", nodecl.} proc `&`*(s: int): cstring {.importcpp: "((#)+'')", nodecl.} proc `&`*(s: bool): cstring {.importcpp: "((#)+'')", nodecl.} +proc `&`*(s: float): cstring {.importcpp: "((#)+'')", nodecl.} proc `&`*(s: cstring): cstring {.importcpp: "(#)", nodecl.} diff --git a/src/vstyles.nim b/src/vstyles.nim index 649c322..161c207 100644 --- a/src/vstyles.nim +++ b/src/vstyles.nim @@ -255,3 +255,11 @@ proc applyStyle*(n: Node; s: VStyle) = n.style = Style() for x in s.mask: n.style.setStyle(toStyleAttrName[x], s.attrs[x]) + +import jstrutils + +proc rgb*(r, g, b: range[0..255]): cstring = + cstring"rgb(" & &r & cstring", " & &g & cstring", " & &b & cstring")" + +proc rgba*(r, g, b: range[0..255], alpha: float): cstring = + cstring"rgb(" & &r & cstring", " & &g & cstring", " & &b & cstring", " & &alpha & cstring")" From 67ee246f40b6c32f96d6bd208addd6ca5a4aa663 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 21 Jun 2017 20:11:41 +0200 Subject: [PATCH 056/247] fixes new rgba proc --- experiments/Untitled Diagram.xml | 1 - src/vstyles.nim | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 experiments/Untitled Diagram.xml diff --git a/experiments/Untitled Diagram.xml b/experiments/Untitled Diagram.xml deleted file mode 100644 index a3019f8..0000000 --- a/experiments/Untitled Diagram.xml +++ /dev/null @@ -1 +0,0 @@ -5VhNc9owEP01PjYjS8bGx4bQ9tDOZIZDk6OwN7YaYTFCfPXXV7ZkbGHDEEJgMuXASE+rlfR230rgkdFs813Sef5LpMA9jNKNRx48jKOBr79LYGuAIB4aIJMsNZDfABP2FyyILLpkKSwcQyUEV2zugokoCkiUg1Epxdo1exHcXXVOM+gAk4TyLvqbpSo36BCHDf4DWJbXK/thbEamNHnNpFgWdj0Pk5fqY4ZntPZlD7rIaSrWLYiMPTKSQijTmm1GwEtqa9rMvG8HRnf7llCoUyZEdhtqWx8dUs2E7QqpcpGJgvJxg95Xx4PSAdK9XM24bvq6CRumnkr4Dg9s97keKpTcmrFB3X22Hv6AUlubA3SphIaahX8KMbc+XhjnI8GFrDaqeYUwSTS+UFK8QmskjeIpKj2bw5UnOkiPhRZiKRNrhW3CUZmBtSK70OiMBzEDvX1tIoFTxVaud2pzL9vZNfzrhg1BfziGHxOO6Eg4AuwE5Au6Q35UI48gmT4EyM8TquBKobJLryhf7hWvVvTWOVMwmdNqv2tdKt0InUvTCqSCzXGiuhTUE4jdp63Nse2um0Ln1+UpbxW5EL2ftJqiFmseDrle4T5lK93MVHVIA03lPqL999j9D1Nny4XSxBX6Yug6mcLJ6+kLR+picfmTHalbhShOKlR1+9m9J57sjObSqMbOrUXDBPpFdj9EEYrOrkXk8rWomvpVSrptGcwFK9Si5fmxBFr6jl1948HeE2DPfifKfnvdMDtoBL47ykmaJ91C6WE9C5FPUS7DPXbwNcul36HoHF0hR1fYEZbvCgu/RVnn6SS43fMq6L2zy1z08adIRkJumY1Rz+X9jvT82DSLu2nmh1fKM//4O/5k3R79HRUH2NUuIuQNrPa+7y9G9bUUHR9UdPDm1/gF9BmgG76tw6PV7XacDK9Xs3S3+e/EvFWa/6fI+B8= \ No newline at end of file diff --git a/src/vstyles.nim b/src/vstyles.nim index 161c207..c5a0931 100644 --- a/src/vstyles.nim +++ b/src/vstyles.nim @@ -262,4 +262,4 @@ proc rgb*(r, g, b: range[0..255]): cstring = cstring"rgb(" & &r & cstring", " & &g & cstring", " & &b & cstring")" proc rgba*(r, g, b: range[0..255], alpha: float): cstring = - cstring"rgb(" & &r & cstring", " & &g & cstring", " & &b & cstring", " & &alpha & cstring")" + cstring"rgba(" & &r & cstring", " & &g & cstring", " & &b & cstring", " & &alpha & cstring")" From 8a2b893d5ed31bf01faba73de9fbd877e99f120d Mon Sep 17 00:00:00 2001 From: Pavel Nuzhdin Date: Thu, 22 Jun 2017 01:09:52 +0700 Subject: [PATCH 057/247] Added onfocus (#29) --- src/vdom.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vdom.nim b/src/vdom.nim index 4edcd4e..be4fb5d 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -47,6 +47,7 @@ type onkeyup, ## A key was released. onkeydown, ## A key is pressed. onkeypressed, # A key was pressed. + onfocus, ## An element got the focus. onblur, ## An element lost the focus. onchange, ## The selected value of an element was changed. onscroll, ## The user scrolled within an element. From bd2d8e7d3f248306ab890859f4c08fc03ba1b632 Mon Sep 17 00:00:00 2001 From: Pavel Nuzhdin Date: Thu, 22 Jun 2017 01:11:54 +0700 Subject: [PATCH 058/247] Added pointer-events style (#30) --- src/kdom.nim | 1 + src/vstyles.nim | 1 + 2 files changed, 2 insertions(+) diff --git a/src/kdom.nim b/src/kdom.nim index f933b87..607d882 100644 --- a/src/kdom.nim +++ b/src/kdom.nim @@ -258,6 +258,7 @@ type paddingTop*: cstring pageBreakAfter*: cstring pageBreakBefore*: cstring + pointerEvents*: cstring position*: cstring right*: cstring scrollbar3dLightColor*: cstring diff --git a/src/vstyles.nim b/src/vstyles.nim index c5a0931..252dda4 100644 --- a/src/vstyles.nim +++ b/src/vstyles.nim @@ -150,6 +150,7 @@ type pageBreakInside perspective perspectiveOrigin + pointerEvents position quotes resize From 051e9985b14496e8951edd59402e01049956daa4 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 21 Jun 2017 21:08:20 +0200 Subject: [PATCH 059/247] added 'karun' tool for convenient testing of karax apps --- tools/karun.nim | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 tools/karun.nim diff --git a/tools/karun.nim b/tools/karun.nim new file mode 100644 index 0000000..dd498ea --- /dev/null +++ b/tools/karun.nim @@ -0,0 +1,41 @@ +## 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 + +const + html = """ + + +$1 + +
+ + + +""" + +proc exec(cmd: string) = + if os.execShellCmd(cmd) != 0: + quit "External command failed: " & cmd + +proc main = + var op = initOptParser() + let rest = op.cmdLineRest + var file = "" + while true: + op.next() + case op.kind + of cmdLongOption, cmdShortOption: discard + of cmdArgument: file = op.key + of cmdEnd: break + + if file.len == 0: quit "filename expected" + let name = file.splitFile.name + createDir("nimcache") + exec("nim js --out:nimcache/" & name & ".js " & rest) + let dest = "nimcache" / name & ".html" + writeFile(dest, html % name) + openDefaultBrowser(dest) + +main() From c8030bc1853ba38e1d1a152f4a696bc78fba0953 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 22 Jun 2017 09:17:19 +0200 Subject: [PATCH 060/247] diff algorithm also diffs components' expansions --- src/karax.nim | 27 ++++++++++++++++++++------- src/vdom.nim | 2 ++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 57c5162..788cd61 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -177,8 +177,7 @@ proc eq(a, b: VNode; deep: bool): EqResult = result = similar if not sameAttrs(a, b): return different if a.class != b.class: return different - # XXX Fixme: smart diffing still is broken for complex apps: - if a.style != b.style: return different # similar + if a.style != b.style: return similar # Do not test event listeners here! return result @@ -206,7 +205,10 @@ proc updateStyles(newNode, oldNode: VNode; deep: bool) = else: oldNode.dom.style = Style() oldNode.style = newNode.style if deep: - assert newNode.len == oldNode.len + if newNode.len != oldNode.len: + kout cstring"argh ", newNode.len, " ", oldNode.len + kout newNode, oldNode + doAssert newNode.len == oldNode.len for i in 0 ..< newNode.len: updateStyles(newNode[i], oldNode[i], deep) @@ -224,8 +226,18 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; if res == changed: assert oldNode.kind == VNodeKind.component let x = VComponent(oldNode) - n = vnodeToDom(x.renderImpl(x), kxi) + let oldExpanded = x.expanded + x.expanded = x.renderImpl(x) x.updatedImpl(x) + if oldExpanded == nil: + n = vnodeToDom(x.expanded, kxi) + else: + if updateElement(parent, current, x.expanded, oldExpanded, kxi): + n = x.expanded.dom + doAssert n != nil, "expanded.dom is nil" + else: + n = oldExpanded.dom + doAssert n != nil, "old expanded.dom is nil" result = true else: detach(oldNode) @@ -234,10 +246,10 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; replaceById(kxi.rootId, n) else: parent.replaceChild(n, current) + elif res == similar: + updateStyles(newNode, oldNode, false) else: - if res == similar: updateStyles(newNode, oldNode, false) newNode.dom = oldNode.dom - if newNode.kind != VNodeKind.text: let newLength = newNode.len var oldLength = oldNode.len @@ -266,8 +278,8 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; of different, changed: break of similar: #updateDom(a, b) + updateStyles(a[i], b, true) a[i] = b - #updateStyles(a, b, true) action while commonPrefix < minLength: @@ -299,6 +311,7 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; for i in pos..oldPos: detach(oldNode[i]) + doAssert pos < current.childNodes.len current.removeChild(current.childNodes[pos]) when false: diff --git a/src/vdom.nim b/src/vdom.nim index 4edcd4e..c11d532 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -129,6 +129,8 @@ type ## should call 'markDirty' instead which is an alias for ## 'inc version'. renderedVersion*: int ## Do not touch. Used by karax. + expanded*: VNode ## Do not touch. Used by karax. The VDOM the component + ## expanded to. proc value*(n: VNode): cstring = n.text proc `value=`*(n: VNode; v: cstring) = n.text = v From 79270c3423a4e690f74c92f55a37c49310bc9c1b Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 22 Jun 2017 09:34:44 +0200 Subject: [PATCH 061/247] fixes #31 --- src/karax.nim | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 788cd61..c03ddbe 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -219,11 +219,11 @@ proc updateDom(newNode, oldNode: VNode) = updateDom(newNode[i], oldNode[i]) proc updateElement(parent, current: Node, newNode, oldNode: VNode; - kxi: KaraxInstance): bool = - let res = eq(newNode, oldNode, deep=false) - if res <= different: + kxi: KaraxInstance): EqResult = + result = eq(newNode, oldNode, deep=false) + if result <= different: var n: Node - if res == changed: + if result == changed: assert oldNode.kind == VNodeKind.component let x = VComponent(oldNode) let oldExpanded = x.expanded @@ -232,13 +232,13 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; if oldExpanded == nil: n = vnodeToDom(x.expanded, kxi) else: - if updateElement(parent, current, x.expanded, oldExpanded, kxi): - n = x.expanded.dom - doAssert n != nil, "expanded.dom is nil" - else: + if updateElement(parent, current, x.expanded, oldExpanded, kxi) >= similar: + x.expanded = oldExpanded n = oldExpanded.dom doAssert n != nil, "old expanded.dom is nil" - result = true + else: + n = x.expanded.dom + doAssert n != nil, "expanded.dom is nil" else: detach(oldNode) n = vnodeToDom(newNode, kxi) @@ -246,7 +246,7 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; replaceById(kxi.rootId, n) else: parent.replaceChild(n, current) - elif res == similar: + elif result == similar: updateStyles(newNode, oldNode, false) else: newNode.dom = oldNode.dom @@ -295,7 +295,8 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; var pos = min(oldPos, newPos) + 1 for i in commonPrefix..pos-1: - if updateElement(current, current.childNodes[i], newNode[i], oldNode[i], kxi): + if updateElement(current, current.childNodes[i], + newNode[i], oldNode[i], kxi) >= similar: newNode[i] = oldNode[i] var nextChildPos = oldPos + 1 From 9aa3ba545ded60775e473529731444a6d3b69acb Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 22 Jun 2017 21:21:54 +0200 Subject: [PATCH 062/247] wip --- src/karax.nim | 46 ++++++++++++++++++++++++++++++++------- src/vdom.nim | 1 + src/vstyles.nim | 12 ++++++++-- tests/blur.nim | 58 +++++++++++++++++++++++++++++++++++++++++++++++++ tools/karun.nim | 14 +++++++++--- 5 files changed, 118 insertions(+), 13 deletions(-) create mode 100644 tests/blur.nim diff --git a/src/karax.nim b/src/karax.nim index c03ddbe..01e5c0f 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -106,7 +106,10 @@ proc vnodeToDom(n: VNode; kxi: KaraxInstance): Node = let x = VComponent(n) if x.onAttachImpl != nil: x.onAttachImpl(x) assert x.renderImpl != nil - result = vnodeToDom(x.renderImpl(x), kxi) + if x.expanded == nil: + x.expanded = x.renderImpl(x) + x.updatedImpl(x) + result = vnodeToDom(x.expanded, kxi) attach n return result else: @@ -133,7 +136,9 @@ proc vnodeToDom(n: VNode; kxi: KaraxInstance): Node = if not n.style.isNil: applyStyle(result, n.style) proc same(n: VNode, e: Node): bool = - if toTag[n.kind] == e.nodename: + if n.kind == VNodeKind.component: + result = same(VComponent(n).expanded, e) + elif toTag[n.kind] == e.nodename: result = true if n.kind != VNodeKind.text: if e.len != n.len: return false @@ -177,7 +182,9 @@ proc eq(a, b: VNode; deep: bool): EqResult = result = similar if not sameAttrs(a, b): return different if a.class != b.class: return different - if a.style != b.style: return similar + if not eq(a.style, b.style): + kout cstring"yes, styles differ" + return similar # Do not test event listeners here! return result @@ -218,11 +225,22 @@ proc updateDom(newNode, oldNode: VNode) = for i in 0 ..< newNode.len: updateDom(newNode[i], oldNode[i]) +proc printV(n: VNode; depth: cstring = "") = + kout depth, cstring($n.kind), n.myid, cstring"key ", n.key + #for k, v in pairs(n.style): + # kout depth, "style: ", k, v + if n.kind == VNodeKind.component: + let nn = VComponent(n) + if nn.expanded != nil: printV(nn.expanded, ">>" & depth) + for i in 0 ..< n.len: + printV(n[i], depth & " ") + proc updateElement(parent, current: Node, newNode, oldNode: VNode; kxi: KaraxInstance): EqResult = result = eq(newNode, oldNode, deep=false) if result <= different: var n: Node + var state = 0 if result == changed: assert oldNode.kind == VNodeKind.component let x = VComponent(oldNode) @@ -231,7 +249,11 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; x.updatedImpl(x) if oldExpanded == nil: n = vnodeToDom(x.expanded, kxi) + state = 1 else: + kout cstring"now comparing components" + printV(oldExpanded) + printV(x.expanded) if updateElement(parent, current, x.expanded, oldExpanded, kxi) >= similar: x.expanded = oldExpanded n = oldExpanded.dom @@ -239,12 +261,16 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; else: n = x.expanded.dom doAssert n != nil, "expanded.dom is nil" + state = 2 + return else: detach(oldNode) n = vnodeToDom(newNode, kxi) + state = 3 if parent == nil: replaceById(kxi.rootId, n) else: + kout cstring"state ", state, parent, current parent.replaceChild(n, current) elif result == similar: updateStyles(newNode, oldNode, false) @@ -295,8 +321,9 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; var pos = min(oldPos, newPos) + 1 for i in commonPrefix..pos-1: - if updateElement(current, current.childNodes[i], - newNode[i], oldNode[i], kxi) >= similar: + let res = updateElement(current, current.childNodes[i], + newNode[i], oldNode[i], kxi) + if res != different: newNode[i] = oldNode[i] var nextChildPos = oldPos + 1 @@ -323,6 +350,9 @@ proc dodraw(kxi: KaraxInstance) = let newtree = kxi.renderer() newtree.id = kxi.rootId kxi.toFocus = nil + #if kxi.currentTree != nil: + # kout cstring"same? ", same(kxi.currentTree, document.getElementById(kxi.rootId)) + kout cstring"dodraw -----------------------------" if kxi.currentTree == nil: kxi.currentTree = newtree let asdom = vnodeToDom(kxi.currentTree, kxi) @@ -330,8 +360,8 @@ proc dodraw(kxi: KaraxInstance) = else: let olddom = document.getElementById(kxi.rootId) discard updateElement(nil, olddom, newtree, kxi.currentTree, kxi) - #assert same(newtree, document.getElementById("ROOT")) kxi.currentTree = newtree + #kout cstring"same? ", same(kxi.currentTree, document.getElementById(kxi.rootId)) if not kxi.postRenderCallback.isNil: kxi.postRenderCallback() @@ -348,10 +378,10 @@ proc redraw*(kxi: KaraxInstance = kxi) = if drawTimeout != nil: clearTimeout(drawTimeout) drawTimeout = setTimeout(dodraw, 30) - elif true: + elif false: reqFrame(proc () = kxi.dodraw) else: - dodraw() + dodraw(kxi) proc init(ev: Event) = reqFrame(proc () = kxi.dodraw) diff --git a/src/vdom.nim b/src/vdom.nim index f022b7f..16b0f36 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -119,6 +119,7 @@ type style*: VStyle ## the style that should be applied to the virtual node. dom*: Node ## the attached real DOM node. Can be 'nil' if the virtual node ## is not part of the virtual DOM anymore. + myid*: int VComponent* = ref object of VNode ## The abstract class for every karax component. renderImpl*: proc(self: VComponent): VNode diff --git a/src/vstyles.nim b/src/vstyles.nim index 252dda4..a6f1996 100644 --- a/src/vstyles.nim +++ b/src/vstyles.nim @@ -213,7 +213,9 @@ type attrs: array[StyleAttr, cstring] mask: set[StyleAttr] -proc `==`*(a, b: VStyle): bool = +proc kout[T](x: T) {.importc: "console.log", varargs.} + +proc eq*(a, b: VStyle): bool = if a.isNil: if b.isNil: return true else: return false @@ -247,9 +249,10 @@ proc setStyle(d: Style; key, val: cstring) {.importcpp: "#[#] = #".} proc merge*(a, b: VStyle): VStyle = ## merges two styles. ``b`` takes precedence over ``a``. - result = VStyle(mask: a.mask + b.mask) + result = VStyle(mask: {}) for i in low(StyleAttr)..high(StyleAttr): result.attrs[i] = if b.attrs[i].isNil: a.attrs[i] else: b.attrs[i] + if result.attrs[i] != nil: incl(result.mask, i) proc applyStyle*(n: Node; s: VStyle) = ## apply the style to the real DOM node ``n``. @@ -257,6 +260,11 @@ proc applyStyle*(n: Node; s: VStyle) = for x in s.mask: n.style.setStyle(toStyleAttrName[x], s.attrs[x]) +iterator pairs*(v: VStyle): (cstring, cstring) = + if v != nil: + for x in v.mask: + yield (toStyleAttrName[x], v.attrs[x]) + import jstrutils proc rgb*(r, g, b: range[0..255]): cstring = diff --git a/tests/blur.nim b/tests/blur.nim new file mode 100644 index 0000000..062c47f --- /dev/null +++ b/tests/blur.nim @@ -0,0 +1,58 @@ +import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils + +type TextInput* = ref object of VComponent + value: cstring + isActive: bool + +var renderId: int +proc render(x: VComponent): VNode = + let self = TextInput(x) + + let style = style( + (StyleAttr.position, cstring"relative"), + (StyleAttr.paddingLeft, cstring"10px"), + (StyleAttr.paddingRight, cstring"5px"), + (StyleAttr.height, cstring"30px"), + (StyleAttr.lineHeight, cstring"30px"), + (StyleAttr.border, cstring"solid 1px " & (if self.isActive: cstring"red" else: cstring"black")), + (StyleAttr.fontSize, cstring"12px"), + (StyleAttr.fontWeight, cstring"600") + ).merge(self.style) + + let inputStyle = style.merge(style( + (StyleAttr.color, cstring"inherit"), + (StyleAttr.fontSize, cstring"inherit"), + (StyleAttr.fontWeight, cstring"inherit"), + (StyleAttr.fontFamily, cstring"inherit"), + (StyleAttr.position, cstring"absolute"), + (StyleAttr.top, cstring"0"), + (StyleAttr.left, cstring"0"), + (StyleAttr.height, cstring"100%"), + (StyleAttr.width, cstring"100%"), + (StyleAttr.border, cstring"none"), + (StyleAttr.backgroundColor, cstring"transparent"), + )) + + proc flip(ev: Event; n: VNode) = + self.isActive = not self.isActive + markDirty(self) + + kout cstring"rendering ", self.myid + inc renderId + result = buildHtml(tdiv(style=style, key=renderId)): + input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip, + key=renderId) + +var gid = 0 +proc newTextInput*(style: VStyle = VStyle(); value: cstring = cstring""): TextInput = + result = newComponent(TextInput, render) + result.style = style + result.value = value + inc gid + result.myid = gid + +proc createDom(): VNode = + result = buildHtml(tdiv): + newTextInput(value=cstring"test") + +setRenderer createDom diff --git a/tools/karun.nim b/tools/karun.nim index dd498ea..a67b9f5 100644 --- a/tools/karun.nim +++ b/tools/karun.nim @@ -21,12 +21,20 @@ proc exec(cmd: string) = proc main = var op = initOptParser() - let rest = op.cmdLineRest + var rest = op.cmdLineRest var file = "" + var run = false while true: op.next() case op.kind - of cmdLongOption, cmdShortOption: discard + of cmdLongOption: + if op.key == "run": + run = true + rest = rest.replace("--run ") + of cmdShortOption: + if op.key == "r": + run = true + rest = rest.replace("-r ") of cmdArgument: file = op.key of cmdEnd: break @@ -36,6 +44,6 @@ proc main = exec("nim js --out:nimcache/" & name & ".js " & rest) let dest = "nimcache" / name & ".html" writeFile(dest, html % name) - openDefaultBrowser(dest) + if run: openDefaultBrowser(dest) main() From cd74cd9d4e31589c21b7012da9894ce2598d93d3 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Fri, 23 Jun 2017 11:09:22 +0200 Subject: [PATCH 063/247] another attempt to fix bug #31 --- src/karax.nim | 39 ++++++++++++++++++++++++--------------- tests/blur.nim | 5 ++--- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 01e5c0f..07d4fd5 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -182,9 +182,7 @@ proc eq(a, b: VNode; deep: bool): EqResult = result = similar if not sameAttrs(a, b): return different if a.class != b.class: return different - if not eq(a.style, b.style): - kout cstring"yes, styles differ" - return similar + if not eq(a.style, b.style): return similar # Do not test event listeners here! return result @@ -212,9 +210,6 @@ proc updateStyles(newNode, oldNode: VNode; deep: bool) = else: oldNode.dom.style = Style() oldNode.style = newNode.style if deep: - if newNode.len != oldNode.len: - kout cstring"argh ", newNode.len, " ", oldNode.len - kout newNode, oldNode doAssert newNode.len == oldNode.len for i in 0 ..< newNode.len: updateStyles(newNode[i], oldNode[i], deep) @@ -232,6 +227,8 @@ proc printV(n: VNode; depth: cstring = "") = if n.kind == VNodeKind.component: let nn = VComponent(n) if nn.expanded != nil: printV(nn.expanded, ">>" & depth) + elif n.kind == VNodeKind.text: + kout depth, n.text for i in 0 ..< n.len: printV(n[i], depth & " ") @@ -251,17 +248,22 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; n = vnodeToDom(x.expanded, kxi) state = 1 else: - kout cstring"now comparing components" - printV(oldExpanded) - printV(x.expanded) - if updateElement(parent, current, x.expanded, oldExpanded, kxi) >= similar: + let res = updateElement(parent, current, x.expanded, oldExpanded, kxi) + if res != different: x.expanded = oldExpanded n = oldExpanded.dom doAssert n != nil, "old expanded.dom is nil" + #kout cstring"produced old DOM: ", cstring($res) + #printV(oldExpanded) + #printV(x.expanded) + return else: n = x.expanded.dom doAssert n != nil, "expanded.dom is nil" state = 2 + #kout cstring"produced new DOM:" + #printV(oldExpanded) + #printV(x.expanded) return else: detach(oldNode) @@ -270,8 +272,9 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; if parent == nil: replaceById(kxi.rootId, n) else: - kout cstring"state ", state, parent, current - parent.replaceChild(n, current) + #kout cstring"state ", state, parent, current + if n != current: + parent.replaceChild(n, current) elif result == similar: updateStyles(newNode, oldNode, false) else: @@ -301,7 +304,9 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; a[i] = b #updateDom(a, b) action - of different, changed: break + of different, changed: + if result != different: result = r + break of similar: #updateDom(a, b) updateStyles(a[i], b, true) @@ -325,6 +330,8 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; newNode[i], oldNode[i], kxi) if res != different: newNode[i] = oldNode[i] + else: + result = different var nextChildPos = oldPos + 1 while pos <= newPos: @@ -336,11 +343,13 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; inc oldLength inc pos inc nextChildPos + result = different for i in pos..oldPos: detach(oldNode[i]) doAssert pos < current.childNodes.len current.removeChild(current.childNodes[pos]) + result = different when false: var drawTimeout: Timeout @@ -352,7 +361,7 @@ proc dodraw(kxi: KaraxInstance) = kxi.toFocus = nil #if kxi.currentTree != nil: # kout cstring"same? ", same(kxi.currentTree, document.getElementById(kxi.rootId)) - kout cstring"dodraw -----------------------------" + #kout cstring"dodraw -----------------------------" if kxi.currentTree == nil: kxi.currentTree = newtree let asdom = vnodeToDom(kxi.currentTree, kxi) @@ -361,7 +370,7 @@ proc dodraw(kxi: KaraxInstance) = let olddom = document.getElementById(kxi.rootId) discard updateElement(nil, olddom, newtree, kxi.currentTree, kxi) kxi.currentTree = newtree - #kout cstring"same? ", same(kxi.currentTree, document.getElementById(kxi.rootId)) + doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) if not kxi.postRenderCallback.isNil: kxi.postRenderCallback() diff --git a/tests/blur.nim b/tests/blur.nim index 062c47f..74b8ba1 100644 --- a/tests/blur.nim +++ b/tests/blur.nim @@ -39,9 +39,8 @@ proc render(x: VComponent): VNode = kout cstring"rendering ", self.myid inc renderId - result = buildHtml(tdiv(style=style, key=renderId)): - input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip, - key=renderId) + result = buildHtml(tdiv(style=style)): + input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip) var gid = 0 proc newTextInput*(style: VStyle = VStyle(); value: cstring = cstring""): TextInput = From 64bc900456d292a282bd0f70fa1f693a149bd8e6 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Fri, 23 Jun 2017 21:03:31 +0200 Subject: [PATCH 064/247] rewritten diff algorithm --- src/karax.nim | 147 +++++++++++++++++++++++++++++++++++++++++++-- src/kdom.nim | 94 +++++++++++++++++++++++++---- src/vdom.nim | 58 +++++++++--------- tests/difftest.nim | 37 ++++++++++++ tests/lists.nim | 18 ++++++ 5 files changed, 310 insertions(+), 44 deletions(-) create mode 100644 tests/difftest.nim create mode 100644 tests/lists.nim diff --git a/src/karax.nim b/src/karax.nim index 07d4fd5..cdb68f7 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -141,9 +141,13 @@ proc same(n: VNode, e: Node): bool = elif toTag[n.kind] == e.nodename: result = true if n.kind != VNodeKind.text: - if e.len != n.len: return false + if e.len != n.len: + kout e.len, n.len + return false for i in 0 ..< n.len: if not same(n[i], e[i]): return false + else: + kout toTag[n.kind], e.nodename proc replaceById(id: cstring; newTree: Node) = let x = document.getElementById(id) @@ -232,6 +236,134 @@ proc printV(n: VNode; depth: cstring = "") = for i in 0 ..< n.len: printV(n[i], depth & " ") +type + PatchKind = enum + pkReplace, pkRemove, pkAppend, pkInsertBefore + Patch = object + k: PatchKind + parent, current, n: Node + +proc addPatch(patches: var seq[Patch]; k: PatchKind; parent, current, n: Node) = + patches.add(Patch(k: k, parent: parent, current: current, n: n)) + +proc apply(patches: seq[Patch]; kxi: KaraxInstance) = + for p in patches: + case p.k + of pkReplace: + if p.parent == nil: + replaceById(kxi.rootId, p.n) + else: + if p.n != p.current: + p.parent.replaceChild(p.n, p.current) + of pkRemove: + p.parent.removeChild(p.current) + of pkAppend: + p.parent.appendChild(p.n) + of pkInsertBefore: + p.parent.insertBefore(p.n, p.current) + +proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch]; + kxi: KaraxInstance): EqResult = + result = eq(newNode, oldNode, deep=false) + if result <= different: + var n: Node + if result == changed: + assert oldNode.kind == VNodeKind.component + let x = VComponent(oldNode) + let oldExpanded = x.expanded + x.expanded = x.renderImpl(x) + x.updatedImpl(x) + if oldExpanded == nil: + n = vnodeToDom(x.expanded, kxi) + else: + let res = diff(parent, current, x.expanded, oldExpanded, patches, kxi) + if res != different: + x.expanded = oldExpanded + n = oldExpanded.dom + doAssert n != nil, "old expanded.dom is nil" + else: + n = x.expanded.dom + doAssert n != nil, "expanded.dom is nil" + return + else: + detach(oldNode) + n = vnodeToDom(newNode, kxi) + patches.addPatch(pkReplace, parent, current, n) + elif result == similar: + updateStyles(newNode, oldNode, false) + else: + newNode.dom = oldNode.dom + let newLength = newNode.len + var oldLength = oldNode.len + let minLength = min(newLength, oldLength) + + assert oldNode.kind == newNode.kind + var commonPrefix = 0 + + template eqAndUpdate(a: VNode; i: int; b: VNode; j: int; info, action: untyped) = + let oldLen = patches.len + if oldNode.kind notin {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}: + assert current != nil + assert current.childNodes[j] != nil, $info + assert oldNode.len == current.len + + let r = if oldNode.kind in {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}: + diff(parent, current, a[i], b[j], patches, kxi) + else: + diff(current, current.childNodes[j], a[i], b[j], patches, kxi) + case r + of identical, changed: + a[i] = b[j] + action + of different: + # undo what 'diff' would have done: + setLen(patches, oldLen) + if result != different: result = r + break + of similar: + updateStyles(a[i], b[j], true) + a[i] = b[j] + action + + while commonPrefix < minLength: + eqAndUpdate(newNode, commonPrefix, oldNode, commonPrefix, cstring"prefix"): + inc commonPrefix + + var oldPos = oldLength - 1 + var newPos = newLength - 1 + while oldPos >= commonPrefix and newPos >= commonPrefix: + eqAndUpdate(newNode, newPos, oldNode, oldPos, cstring"suffix"): + dec oldPos + dec newPos + + var pos = min(oldPos, newPos) + 1 + for i in commonPrefix..pos-1: + let res = diff(current, current.childNodes[i], + newNode[i], oldNode[i], patches, kxi) + if res != different: + newNode[i] = oldNode[i] + else: + result = different + + var nextChildPos = oldPos + 1 + if nextChildPos == oldLength: + for i in pos..newPos: + patches.addPatch(pkAppend, current, nil, vnodeToDom(newNode[i], kxi)) + result = different + else: + let before = current.childNodes[nextChildPos] + for i in pos..newPos: + patches.addPatch(pkInsertBefore, current, before, + vnodeToDom(newNode[i], kxi)) + result = different + # XXX call 'attach' here? + for i in pos..oldPos: + detach(oldNode[i]) + doAssert i < current.childNodes.len + patches.addPatch(pkRemove, current, current.childNodes[i], nil) + result = different + + proc updateElement(parent, current: Node, newNode, oldNode: VNode; kxi: KaraxInstance): EqResult = result = eq(newNode, oldNode, deep=false) @@ -368,9 +500,14 @@ proc dodraw(kxi: KaraxInstance) = replaceById(kxi.rootId, asdom) else: let olddom = document.getElementById(kxi.rootId) - discard updateElement(nil, olddom, newtree, kxi.currentTree, kxi) + when false: + discard updateElement(nil, olddom, newtree, kxi.currentTree, kxi) + else: + var patches: seq[Patch] = @[] + discard diff(nil, olddom, newtree, kxi.currentTree, patches, kxi) + patches.apply(kxi) kxi.currentTree = newtree - doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) + #doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) if not kxi.postRenderCallback.isNil: kxi.postRenderCallback() @@ -387,7 +524,7 @@ proc redraw*(kxi: KaraxInstance = kxi) = if drawTimeout != nil: clearTimeout(drawTimeout) drawTimeout = setTimeout(dodraw, 30) - elif false: + elif true: reqFrame(proc () = kxi.dodraw) else: dodraw(kxi) @@ -442,7 +579,7 @@ proc prepend(parent, kid: Element) = parent.insertBefore(kid, parent.firstChild) proc loadScript*(jsfilename: cstring; kxi: KaraxInstance = kxi) = - let body = getElementById("body") + let body = document.getElementById("body") let s = document.createElement("script") s.setAttr "type", "text/javascript" s.setAttr "src", jsfilename diff --git a/src/kdom.nim b/src/kdom.nim index 607d882..6799976 100644 --- a/src/kdom.nim +++ b/src/kdom.nim @@ -94,6 +94,8 @@ type previousSibling*: Node innerHTML*: cstring style*: Style + when defined(nodejs): + id*: cstring Document* = ref DocumentObj DocumentObj {.importc.} = object of NodeObj @@ -395,12 +397,87 @@ type TimeOut* {.importc.} = ref object of RootObj Interval* {.importc.} = object of RootObj -proc len*(x: Node): int {.importcpp: "#.childNodes.length".} -proc `[]`*(x: Node; idx: int): Element {.importcpp: "#.childNodes[#]".} +when defined(nodejs): + # we provide a dummy DOM for nodejs for testing purposes + proc len*(x: Node): int = x.childNodes.len + proc `[]`*(x: Node; idx: int): Element = + assert idx >= 0 and idx < x.childNodes.len + result = cast[Element](x.childNodes[idx]) + + var document* = Document(nodeType: DocumentNode) + + proc getElem(x: Element; id: cstring): Element = + if x.id == id: return x + for i in 0.." -proc calcHash*(n: VNode) = - if n.validHash: return - n.validHash = true - var h: Hash = ord n.kind - if n.id != nil: - h &= "id" - h &= n.id - if n.class != nil: - h &= "class" - h &= n.class - if n.key >= 0: - h &= "k" - h &= n.key - for k, v in attrs(n): - h &= " " - h &= k - h &= "=" - h &= v - if n.kind == VNodeKind.text or n.text != nil: - h &= "t" - h &= n.text - else: - for child in items(n): - calcHash(child) - h &= child.hash - n.hash = h +when false: + proc calcHash*(n: VNode) = + if n.validHash: return + n.validHash = true + var h: Hash = ord n.kind + if n.id != nil: + h &= "id" + h &= n.id + if n.class != nil: + h &= "class" + h &= n.class + if n.key >= 0: + h &= "k" + h &= n.key + for k, v in attrs(n): + h &= " " + h &= k + h &= "=" + h &= v + if n.kind == VNodeKind.text or n.text != nil: + h &= "t" + h &= n.text + else: + for child in items(n): + calcHash(child) + h &= child.hash + n.hash = h proc `$`*(n: VNode): cstring = var res = "" diff --git a/tests/difftest.nim b/tests/difftest.nim new file mode 100644 index 0000000..9f80037 --- /dev/null +++ b/tests/difftest.nim @@ -0,0 +1,37 @@ + +include "../src/karax" +import "../src/karaxdsl" + +proc doDiff(a, b: VNode) = + var patches: seq[Patch] = @[] + echo diff(nil, vnodeToDom(a, kxi), b, a, patches, kxi) + echo patches + +proc testAppend() = + let a = buildHtml(tdiv): + ul: + li: text "A" + li: text "B" + let b = buildHtml(tdiv): + ul: + li: text "A" + li: text "B" + li: text "C" + doDiff(a, b) + +proc testInsert() = + let a = buildHtml(tdiv): + ul: + li: text "A" + li: text "C" + let b = buildHtml(tdiv): + ul: + li: text "A" + li: text "B" + li: text "C" + doDiff(a, b) + +kxi = KaraxInstance(rootId: cstring"ROOT", renderer: proc (): VNode = discard) + +#testAppend() +testInsert() diff --git a/tests/lists.nim b/tests/lists.nim new file mode 100644 index 0000000..cce9abf --- /dev/null +++ b/tests/lists.nim @@ -0,0 +1,18 @@ +import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils + +var contents: seq[cstring] = @[] + +proc onTodoEnter(e: Event; n: VNode) = + contents.insert(n.value & "BBBB") + contents.insert(n.value) + n.value = "" + +proc createDom(): VNode = + result = buildHtml(tdiv): + input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo", + onkeyupenter = onTodoEnter, setFocus) + for c in contents: + tdiv: + text c + +setRenderer createDom From f89da765fab9a45873374e1f59015dd1065ba08b Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Fri, 23 Jun 2017 21:33:48 +0200 Subject: [PATCH 065/247] another bugfix for smart-diffing --- src/karax.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/karax.nim b/src/karax.nim index cdb68f7..36a0506 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -321,7 +321,7 @@ proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch if result != different: result = r break of similar: - updateStyles(a[i], b[j], true) + updateStyles(a[i], b[j], false) a[i] = b[j] action From 74c07f063444ebfeebd20345da4923c20734c32d Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Fri, 23 Jun 2017 22:21:06 +0200 Subject: [PATCH 066/247] code cleanups --- src/karax.nim | 256 +++++++++----------------------------------------- 1 file changed, 45 insertions(+), 211 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 36a0506..00863c6 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -152,13 +152,12 @@ proc same(n: VNode, e: Node): bool = proc replaceById(id: cstring; newTree: Node) = let x = document.getElementById(id) x.parentNode.replaceChild(newTree, x) - #newTree.id = id type EqResult = enum changed, different, similar, identical -proc eq(a, b: VNode; deep: bool): EqResult = +proc eq(a, b: VNode): EqResult = if a.kind != b.kind: return different if a.id != b.id: return different result = identical @@ -169,60 +168,25 @@ proc eq(a, b: VNode; deep: bool): EqResult = if a.text != b.text: return different if a.len != b.len: return different for i in 0..= 0 and isDirty(newNode.key): - unmarkDirty(newNode.key) - let n = vnodeToDom(newNode, kxi) - if parent == nil: - replaceById(kxi.rootId, n) - else: - parent.replaceChild(n, current) - elif newNode.kind != VNodeKind.text and newNode.kind != VNodeKind.vthunk and - newNode.kind != VNodeKind.dthunk: - for i in 0..newNode.len-1: - updateDirtyElements(current, current[i], newNode[i], kxi) - # leave early if we know there cannot be anything left to do: - #if dirtyCount <= 0: return - -proc updateStyles(newNode, oldNode: VNode; deep: bool) = +proc updateStyles(newNode, oldNode: VNode) = # we keep the oldNode, but take over the style from the new node: if oldNode.dom != nil: if newNode.style != nil: applyStyle(oldNode.dom, newNode.style) else: oldNode.dom.style = Style() oldNode.style = newNode.style - if deep: - doAssert newNode.len == oldNode.len - for i in 0 ..< newNode.len: - updateStyles(newNode[i], oldNode[i], deep) - -proc updateDom(newNode, oldNode: VNode) = - newNode.dom = oldNode.dom - assert newNode.len == oldNode.len - for i in 0 ..< newNode.len: - updateDom(newNode[i], oldNode[i]) proc printV(n: VNode; depth: cstring = "") = kout depth, cstring($n.kind), n.myid, cstring"key ", n.key @@ -264,55 +228,32 @@ proc apply(patches: seq[Patch]; kxi: KaraxInstance) = proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch]; kxi: KaraxInstance): EqResult = - result = eq(newNode, oldNode, deep=false) - if result <= different: - var n: Node - if result == changed: - assert oldNode.kind == VNodeKind.component - let x = VComponent(oldNode) - let oldExpanded = x.expanded - x.expanded = x.renderImpl(x) - x.updatedImpl(x) - if oldExpanded == nil: - n = vnodeToDom(x.expanded, kxi) - else: - let res = diff(parent, current, x.expanded, oldExpanded, patches, kxi) - if res != different: - x.expanded = oldExpanded - n = oldExpanded.dom - doAssert n != nil, "old expanded.dom is nil" - else: - n = x.expanded.dom - doAssert n != nil, "expanded.dom is nil" - return - else: - detach(oldNode) - n = vnodeToDom(newNode, kxi) - patches.addPatch(pkReplace, parent, current, n) - elif result == similar: - updateStyles(newNode, oldNode, false) - else: + result = eq(newNode, oldNode) + case result + of identical: newNode.dom = oldNode.dom let newLength = newNode.len var oldLength = oldNode.len let minLength = min(newLength, oldLength) + if minLength == 0: return result assert oldNode.kind == newNode.kind var commonPrefix = 0 template eqAndUpdate(a: VNode; i: int; b: VNode; j: int; info, action: untyped) = let oldLen = patches.len - if oldNode.kind notin {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}: - assert current != nil - assert current.childNodes[j] != nil, $info - assert oldNode.len == current.len + when false: + if oldNode.kind notin {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}: + assert current != nil + assert current.childNodes[j] != nil, $info + assert oldNode.len == current.len let r = if oldNode.kind in {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}: diff(parent, current, a[i], b[j], patches, kxi) else: diff(current, current.childNodes[j], a[i], b[j], patches, kxi) case r - of identical, changed: + of identical, changed, similar: a[i] = b[j] action of different: @@ -320,10 +261,10 @@ proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch setLen(patches, oldLen) if result != different: result = r break - of similar: - updateStyles(a[i], b[j], false) - a[i] = b[j] - action + #of similar: + # updateStyles(a[i], b[j]) + # a[i] = b[j] + # action while commonPrefix < minLength: eqAndUpdate(newNode, commonPrefix, oldNode, commonPrefix, cstring"prefix"): @@ -338,20 +279,18 @@ proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch var pos = min(oldPos, newPos) + 1 for i in commonPrefix..pos-1: - let res = diff(current, current.childNodes[i], - newNode[i], oldNode[i], patches, kxi) - if res != different: + if diff(current, current.childNodes[i], + newNode[i], oldNode[i], patches, kxi) != different: newNode[i] = oldNode[i] else: result = different - var nextChildPos = oldPos + 1 - if nextChildPos == oldLength: + if oldPos + 1 == oldLength: for i in pos..newPos: patches.addPatch(pkAppend, current, nil, vnodeToDom(newNode[i], kxi)) result = different else: - let before = current.childNodes[nextChildPos] + let before = current.childNodes[oldPos + 1] for i in pos..newPos: patches.addPatch(pkInsertBefore, current, before, vnodeToDom(newNode[i], kxi)) @@ -359,153 +298,48 @@ proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch # XXX call 'attach' here? for i in pos..oldPos: detach(oldNode[i]) - doAssert i < current.childNodes.len + #doAssert i < current.childNodes.len patches.addPatch(pkRemove, current, current.childNodes[i], nil) result = different - -proc updateElement(parent, current: Node, newNode, oldNode: VNode; - kxi: KaraxInstance): EqResult = - result = eq(newNode, oldNode, deep=false) - if result <= different: - var n: Node - var state = 0 - if result == changed: - assert oldNode.kind == VNodeKind.component - let x = VComponent(oldNode) - let oldExpanded = x.expanded - x.expanded = x.renderImpl(x) - x.updatedImpl(x) - if oldExpanded == nil: - n = vnodeToDom(x.expanded, kxi) - state = 1 - else: - let res = updateElement(parent, current, x.expanded, oldExpanded, kxi) - if res != different: - x.expanded = oldExpanded - n = oldExpanded.dom - doAssert n != nil, "old expanded.dom is nil" - #kout cstring"produced old DOM: ", cstring($res) - #printV(oldExpanded) - #printV(x.expanded) - return - else: - n = x.expanded.dom - doAssert n != nil, "expanded.dom is nil" - state = 2 - #kout cstring"produced new DOM:" - #printV(oldExpanded) - #printV(x.expanded) - return - else: + of similar: + updateStyles(newNode, oldNode) + of changed: + assert oldNode.kind == VNodeKind.component + let x = VComponent(oldNode) + let oldExpanded = x.expanded + x.expanded = x.renderImpl(x) + x.updatedImpl(x) + if oldExpanded == nil: detach(oldNode) - n = vnodeToDom(newNode, kxi) - state = 3 - if parent == nil: - replaceById(kxi.rootId, n) + let n = vnodeToDom(x.expanded, kxi) + patches.addPatch(pkReplace, parent, current, n) else: - #kout cstring"state ", state, parent, current - if n != current: - parent.replaceChild(n, current) - elif result == similar: - updateStyles(newNode, oldNode, false) - else: - newNode.dom = oldNode.dom - if newNode.kind != VNodeKind.text: - let newLength = newNode.len - var oldLength = oldNode.len - let minLength = min(newLength, oldLength) - assert oldNode.kind == newNode.kind - when defined(simpleDiff): - for i in 0..min(newLength, oldLength)-1: - updateElement(current, current[i], newNode[i], oldNode[i], kxi) - if newLength > oldLength: - for i in oldLength..newLength-1: - current.appendChild(vnodeToDom(newNode[i])) - elif oldLength > newLength: - for i in countdown(oldLength-1, newLength): - detach(oldNode[i]) - current.removeChild(current.lastChild) + let res = diff(parent, current, x.expanded, oldExpanded, patches, kxi) + if res != different: + x.expanded = oldExpanded + assert oldExpanded.dom != nil, "old expanded.dom is nil" else: - var commonPrefix = 0 - - template eqAndUpdate(a: VNode; i: int; b: VNode; action: untyped) = - let r = eq(a[i], b, true) - case r - of identical: - a[i] = b - #updateDom(a, b) - action - of different, changed: - if result != different: result = r - break - of similar: - #updateDom(a, b) - updateStyles(a[i], b, true) - a[i] = b - action - - while commonPrefix < minLength: - eqAndUpdate(newNode, commonPrefix, oldNode[commonPrefix]): - inc commonPrefix - - var oldPos = oldLength - 1 - var newPos = newLength - 1 - while oldPos >= commonPrefix and newPos >= commonPrefix: - eqAndUpdate(newNode, newPos, oldNode[oldPos]): - dec oldPos - dec newPos - - var pos = min(oldPos, newPos) + 1 - for i in commonPrefix..pos-1: - let res = updateElement(current, current.childNodes[i], - newNode[i], oldNode[i], kxi) - if res != different: - newNode[i] = oldNode[i] - else: - result = different - - var nextChildPos = oldPos + 1 - while pos <= newPos: - if nextChildPos == oldLength: - current.appendChild(vnodeToDom(newNode[pos], kxi)) - else: - current.insertBefore(vnodeToDom(newNode[pos], kxi), current.childNodes[nextChildPos]) - # added new Node, so old state of VDOM have one more Node - inc oldLength - inc pos - inc nextChildPos - result = different - - for i in pos..oldPos: - detach(oldNode[i]) - doAssert pos < current.childNodes.len - current.removeChild(current.childNodes[pos]) - result = different - -when false: - var drawTimeout: Timeout + assert x.expanded.dom != nil, "expanded.dom is nil" + of different: + detach(oldNode) + let n = vnodeToDom(newNode, kxi) + patches.addPatch(pkReplace, parent, current, n) proc dodraw(kxi: KaraxInstance) = if kxi.renderer.isNil: return let newtree = kxi.renderer() newtree.id = kxi.rootId kxi.toFocus = nil - #if kxi.currentTree != nil: - # kout cstring"same? ", same(kxi.currentTree, document.getElementById(kxi.rootId)) - #kout cstring"dodraw -----------------------------" if kxi.currentTree == nil: kxi.currentTree = newtree let asdom = vnodeToDom(kxi.currentTree, kxi) replaceById(kxi.rootId, asdom) else: let olddom = document.getElementById(kxi.rootId) - when false: - discard updateElement(nil, olddom, newtree, kxi.currentTree, kxi) - else: - var patches: seq[Patch] = @[] - discard diff(nil, olddom, newtree, kxi.currentTree, patches, kxi) - patches.apply(kxi) + var patches: seq[Patch] = @[] + discard diff(nil, olddom, newtree, kxi.currentTree, patches, kxi) + patches.apply(kxi) kxi.currentTree = newtree #doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) From 4bfe66edd6de36657efe6e3e6de12bb1d026b8b2 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Sat, 24 Jun 2017 08:22:31 +0200 Subject: [PATCH 067/247] optimized VStyle representation --- src/jdict.nim | 9 +++++ src/vstyles.nim | 95 ++++++++++++++++++++++++++++++++----------------- 2 files changed, 72 insertions(+), 32 deletions(-) diff --git a/src/jdict.nim b/src/jdict.nim index 0eecb58..f541ac7 100644 --- a/src/jdict.nim +++ b/src/jdict.nim @@ -11,3 +11,12 @@ proc contains*[K, V](d: JDict[K, V], k: K): bool {.importcpp: "#.hasOwnProperty( proc del*[K, V](d: JDict[K, V], k: K) {.importcpp: "delete #[#]".} +type + JSeq*[T] = ref object + +proc `[]`*[T](s: JSeq[T], i: int): T {.importcpp: "#[#]", noSideEffect.} +proc `[]=`*[T](s: JSeq[T], i: int, v: T) {.importcpp: "#[#] = #", noSideEffect.} + +proc newJSeq*[T](): JSeq[T] {.importcpp: "[@]".} +proc len*[T](s: JSeq[T]): int {.importcpp: "#.length", noSideEffect.} +proc add*[T](s: JSeq[T]; x: T) {.importcpp: "#.push(#)", noSideEffect.} diff --git a/src/vstyles.nim b/src/vstyles.nim index a6f1996..2f854e1 100644 --- a/src/vstyles.nim +++ b/src/vstyles.nim @@ -1,5 +1,5 @@ -import kdom, macros +import kdom, macros, jdict type StyleAttr* {.pure.} = enum @@ -208,62 +208,93 @@ macro buildLookupTables(): untyped = buildLookupTables() -type - VStyle* = ref object - attrs: array[StyleAttr, cstring] - mask: set[StyleAttr] +# I optimized the heck out of this representation since profiling showed +# it's relevant. +# even index: key, odd index: value; done this way for memory efficiency: -proc kout[T](x: T) {.importc: "console.log", varargs.} +type + VStyle* = JSeq[cstring] proc eq*(a, b: VStyle): bool = if a.isNil: if b.isNil: return true else: return false elif b.isNil: return false - if a.mask != b.mask: return false - for x in a.mask: - if a.attrs[x] != b.attrs[x]: return false + if a.len != b.len: return false + for i in 0.. a: + s.add nil + s.add nil + # 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 + s[i+1] = value + return + inc i, 2 + s.add a + s.add value + +proc setAttr*(s: VStyle; attr: StyleAttr, value: cstring) {.noSideEffect.} = assert value != nil, "value must not be nil" - s.attrs[attr] = value - incl(s.mask, attr) + setAttr(s, toStyleAttrName[attr], value) -proc getAttr*(s: VStyle; attr: StyleAttr): cstring = +proc getAttr*(s: VStyle; attr: StyleAttr): cstring {.noSideEffect.} = ## returns 'nil' if the attribute has not been set. - result = s.attrs[attr] + var i = 0 + let a = toStyleAttrName[attr] + while i < s.len: + if s[i] == a: + return s[i+1] + elif s[i] > a: + return nil + inc i, 2 -proc style*(pairs: varargs[(StyleAttr, cstring)]): VStyle = +proc style*(pairs: varargs[(StyleAttr, cstring)]): VStyle {.noSideEffect.} = ## constructs a VStyle object from a list of (attribute, value)-pairs. - result = VStyle(mask: {}) + result = newJSeq[cstring]() for x in pairs: result.setAttr x[0], x[1] -proc style*(a: StyleAttr; val: cstring): VStyle = +proc style*(a: StyleAttr; val: cstring): VStyle {.noSideEffect.} = ## constructs a VStyle object from a single (attribute, value)-pair. - result = VStyle(mask: {}) + result = newJSeq[cstring]() result.setAttr a, val -proc setStyle(d: Style; key, val: cstring) {.importcpp: "#[#] = #".} +proc setStyle(d: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.} -proc merge*(a, b: VStyle): VStyle = +proc merge*(a, b: VStyle): VStyle {.noSideEffect.} = ## merges two styles. ``b`` takes precedence over ``a``. - result = VStyle(mask: {}) - for i in low(StyleAttr)..high(StyleAttr): - result.attrs[i] = if b.attrs[i].isNil: a.attrs[i] else: b.attrs[i] - if result.attrs[i] != nil: incl(result.mask, i) + result = newJSeq[cstring]() + for i in 0.. Date: Sat, 24 Jun 2017 08:23:22 +0200 Subject: [PATCH 068/247] optimized the new diff algorihtm --- src/karax.nim | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 00863c6..3f3ebed 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -205,26 +205,30 @@ type pkReplace, pkRemove, pkAppend, pkInsertBefore Patch = object k: PatchKind - parent, current, n: Node + parent, current: Node + n: VNode -proc addPatch(patches: var seq[Patch]; k: PatchKind; parent, current, n: Node) = +proc addPatch(patches: var seq[Patch]; k: PatchKind; parent, current: Node; + n: VNode) = patches.add(Patch(k: k, parent: parent, current: current, n: n)) proc apply(patches: seq[Patch]; kxi: KaraxInstance) = for p in patches: case p.k of pkReplace: + let nn = vnodeToDom(p.n, kxi) if p.parent == nil: - replaceById(kxi.rootId, p.n) + replaceById(kxi.rootId, nn) else: - if p.n != p.current: - p.parent.replaceChild(p.n, p.current) + p.parent.replaceChild(nn, p.current) of pkRemove: p.parent.removeChild(p.current) of pkAppend: - p.parent.appendChild(p.n) + let nn = vnodeToDom(p.n, kxi) + p.parent.appendChild(nn) of pkInsertBefore: - p.parent.insertBefore(p.n, p.current) + let nn = vnodeToDom(p.n, kxi) + p.parent.insertBefore(nn, p.current) proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch]; kxi: KaraxInstance): EqResult = @@ -248,7 +252,8 @@ proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch assert current.childNodes[j] != nil, $info assert oldNode.len == current.len - let r = if oldNode.kind in {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}: + let r = if oldNode.kind == VNodeKind.component or oldNode.kind == VNodeKind.vthunk or + oldNode.kind == VNodeKind.dthunk: diff(parent, current, a[i], b[j], patches, kxi) else: diff(current, current.childNodes[j], a[i], b[j], patches, kxi) @@ -287,13 +292,12 @@ proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch if oldPos + 1 == oldLength: for i in pos..newPos: - patches.addPatch(pkAppend, current, nil, vnodeToDom(newNode[i], kxi)) + patches.addPatch(pkAppend, current, nil, newNode[i]) result = different else: let before = current.childNodes[oldPos + 1] for i in pos..newPos: - patches.addPatch(pkInsertBefore, current, before, - vnodeToDom(newNode[i], kxi)) + patches.addPatch(pkInsertBefore, current, before, newNode[i]) result = different # XXX call 'attach' here? for i in pos..oldPos: @@ -312,8 +316,7 @@ proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch x.updatedImpl(x) if oldExpanded == nil: detach(oldNode) - let n = vnodeToDom(x.expanded, kxi) - patches.addPatch(pkReplace, parent, current, n) + patches.addPatch(pkReplace, parent, current, x.expanded) else: let res = diff(parent, current, x.expanded, oldExpanded, patches, kxi) if res != different: @@ -323,8 +326,7 @@ proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch assert x.expanded.dom != nil, "expanded.dom is nil" of different: detach(oldNode) - let n = vnodeToDom(newNode, kxi) - patches.addPatch(pkReplace, parent, current, n) + patches.addPatch(pkReplace, parent, current, newNode) proc dodraw(kxi: KaraxInstance) = if kxi.renderer.isNil: return From 8a1424669f6269db0b1ba94d3ab43b75270272bd Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 26 Jun 2017 21:03:39 +0200 Subject: [PATCH 069/247] optimized diff algorithm further; fixed a stupid bug; cleaned up the tests --- src/jdict.nim | 2 + src/karax.nim | 30 +++--- tests/diffDomTests.html | 1 - tests/diffDomTests.nim | 223 +++++++++++++--------------------------- tests/difftest.nim | 29 +++++- 5 files changed, 117 insertions(+), 168 deletions(-) diff --git a/src/jdict.nim b/src/jdict.nim index f541ac7..b239af2 100644 --- a/src/jdict.nim +++ b/src/jdict.nim @@ -20,3 +20,5 @@ proc `[]=`*[T](s: JSeq[T], i: int, v: T) {.importcpp: "#[#] = #", noSideEffect.} proc newJSeq*[T](): JSeq[T] {.importcpp: "[@]".} proc len*[T](s: JSeq[T]): int {.importcpp: "#.length", noSideEffect.} proc add*[T](s: JSeq[T]; x: T) {.importcpp: "#.push(#)", noSideEffect.} + +proc shrink*[T](s: JSeq[T]; shorterLen: int) {.importcpp: "#.length = #", noSideEffect.} diff --git a/src/karax.nim b/src/karax.nim index 3f3ebed..f07bf71 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -208,12 +208,13 @@ type parent, current: Node n: VNode -proc addPatch(patches: var seq[Patch]; k: PatchKind; parent, current: Node; +proc addPatch(patches: JSeq[Patch]; k: PatchKind; parent, current: Node; n: VNode) = patches.add(Patch(k: k, parent: parent, current: current, n: n)) -proc apply(patches: seq[Patch]; kxi: KaraxInstance) = - for p in patches: +proc apply(patches: JSeq[Patch]; kxi: KaraxInstance) = + for i in 0.. Diff dom tests - diff --git a/tests/diffDomTests.nim b/tests/diffDomTests.nim index d2c09ab..e28c6f6 100644 --- a/tests/diffDomTests.nim +++ b/tests/diffDomTests.nim @@ -2,182 +2,107 @@ import kdom, vdom, times, karax, karaxdsl, jdict, jstrutils, parseutils, sequtils var - entries: seq[cstring] - results: seq[cstring] - timeout: Timeout + entries: seq[cstring] + results: seq[cstring] proc reset() = - results.add cstring"reset started" - entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5") ] - redraw() - results.add cstring"reset finished" + entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")] + redrawSync() -proc checkOrder(order : seq[int]): bool = - var ul = getElementById("ul") - if ul == nil or len(ul.children) != len(order): - return false - var pos = 0 - for child in ul.children: - if child.id != $order[pos]: - return false - inc pos - return true +proc checkOrder(order: seq[int]): bool = + var ul = getElementById("ul") + if ul == nil or len(ul.children) != len(order): + kout ul, len(order) + return false + var pos = 0 + for child in ul.children: + if child.id != $order[pos]: + kout pos + return false + inc pos + return true -proc check1() = - let result = checkOrder(@[0, 1, 2, 3, 4, 7, 5]) - if result: - results.add cstring"test1 - OK" - else: - results.add cstring"test1 - FAIL" +proc check(name: cstring; order: seq[int]) = + let result = checkOrder(order) + results.add name & (if result: cstring" - OK" else: cstring" -FAIL") # result: 0 1 2 3 4 7 5 proc test1() = - results.add cstring"test1 started" - entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5") ] - entries.insert(cstring("7"), 5) - redraw() - timeout = setTimeout(check1, 20) + results.add cstring"test1 started" + entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")] + entries.insert(cstring("7"), 5) + redrawSync() + check("test1", @[0, 1, 2, 3, 4, 7, 5]) -proc check2() = - let result = checkOrder(@[8, 0, 1, 2, 3, 4, 7, 5]) - if result: - results.add cstring"test1 - OK" - else: - results.add cstring"test2 - FAIL" - -# result: 8 0 1 2 3 4 7 5 proc test2() = - results.add cstring"test2 started" - entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5") ] - entries.insert(cstring("7"), 5) - entries.insert(cstring("8"), 0) - redraw() - timeout = setTimeout(check2, 20) + results.add cstring"test2 started" + entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")] + entries.insert(cstring("7"), 5) + entries.insert(cstring("8"), 0) + redrawSync() + check("test2", @[8, 0, 1, 2, 3, 4, 7, 5]) -proc check3() = - let result = checkOrder(@[2, 3, 4, 1]) - if result: - results.add cstring"test3 - OK" - else: - results.add cstring"test3 - FAIL" - -# result: 2 3 4 1 proc test3() = - results.add cstring"test3 started" - entries = @[cstring("2"), cstring("3"), cstring("4"), cstring("1") ] - redraw() - timeout = setTimeout(check3, 20) + results.add cstring"test3 started" + entries = @[cstring("2"), cstring("3"), cstring("4"), cstring("1")] + redrawSync() + check("test3", @[2, 3, 4, 1]) -proc check4() = - let result = checkOrder(@[5, 6, 7, 8]) - if result: - results.add cstring"test4 - OK" - else: - results.add cstring"test4 - FAIL" - -# result: 5 6 7 8 proc test4() = - results.add cstring"test4 started" - entries = @[cstring("5"), cstring("6"), cstring("7"), cstring("8") ] - redraw() - timeout = setTimeout(check4, 20) + results.add cstring"test4 started" + entries = @[cstring("5"), cstring("6"), cstring("7"), cstring("8") ] + redrawSync() + check("test4", @[5, 6, 7, 8]) -proc check5() = - let result = checkOrder(@[0, 1, 3, 5, 4, 5]) - if result: - results.add cstring"test5 - OK" - else: - results.add cstring"test5 - FAIL" - -# result: 0 1 3 5 4 5 proc test5() = - results.add cstring"test5 started" - entries = @[cstring("0"), cstring("1"), cstring("3"), cstring("5"), cstring("4"), cstring("5") ] - redraw() - timeout = setTimeout(check5, 20) + results.add cstring"test5 started" + entries = @[cstring("0"), cstring("1"), cstring("3"), cstring("5"), cstring("4"), cstring("5")] + redrawSync() + check("test 5", @[0, 1, 3, 5, 4, 5]) -proc check6() = - let result = checkOrder(@[]) - if result: - results.add cstring"test6 - OK" - else: - results.add cstring"test6 - FAIL" - -# result: empty proc test6() = - results.add cstring"test6 started" - entries = @[] - redraw() - timeout = setTimeout(check6, 20) - -proc check7() = - let result = checkOrder(@[2]) - if result: - results.add cstring"test7 - OK" - else: - results.add cstring"test7 - FAIL" - redraw() + results.add cstring"test6 started" + entries = @[] + redrawSync() + check("test 6", @[]) # result: 2 proc test7() = - results.add cstring"test7 started" - entries = @[cstring("2")] - redraw() - timeout = setTimeout(check7, 20) + results.add cstring"test7 started" + entries = @[cstring("2")] + redrawSync() + check("test 7", @[2]) proc createEntry(id: int): VNode = result = buildHtml(): button(id="" & $id): - text $id + text $id proc createDom(): VNode = - result = buildHtml(tdiv()): - ul(id="ul"): - for e in entries: - createEntry(parseInt(e)) - for r in results: - tdiv: - text r + result = buildHtml(tdiv()): + ul(id="ul"): + for e in entries: + createEntry(parseInt(e)) + for r in results: + tdiv: + text r proc onload() = - for i in 0..5: # 0_000: - entries.add(cstring($i)) + for i in 0..5: # 0_000: + entries.add(cstring($i)) + test1() + reset() + test2() + reset() + test3() + reset() + test4() + reset() + test5() + reset() + test6() + reset() + test7() - var dtReset = 100 - var dtTest = 500 - - var t = dtReset - timeout = setTimeout(test1, t) - t += dtTest - timeout = setTimeout(reset, t) - t += dtReset - - timeout = setTimeout(test2, t) - t += dtTest - timeout = setTimeout(reset, t) - t += dtReset - - timeout = setTimeout(test3, t) - t += dtTest - timeout = setTimeout(reset, t) - t += dtReset - - timeout = setTimeout(test4, t) - t += dtTest - timeout = setTimeout(reset, t) - t += dtReset - - timeout = setTimeout(test5, t) - t += dtTest - timeout = setTimeout(reset, t) - t += dtReset - - timeout = setTimeout(test6, t) - t += dtTest - timeout = setTimeout(reset, t) - t += dtReset - - timeout = setTimeout(test7, t) - -onload() setRenderer createDom +onload() diff --git a/tests/difftest.nim b/tests/difftest.nim index 9f80037..bd90b6a 100644 --- a/tests/difftest.nim +++ b/tests/difftest.nim @@ -2,10 +2,19 @@ include "../src/karax" import "../src/karaxdsl" +proc hasDom(n: Vnode) = + if n.kind in {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}: + discard + else: + doAssert n.dom != nil + for i in 0.. Date: Tue, 27 Jun 2017 23:54:58 +0200 Subject: [PATCH 070/247] smart-diff algorithm works now --- src/karax.nim | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index f07bf71..f8230c7 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -234,8 +234,10 @@ proc apply(patches: JSeq[Patch]; kxi: KaraxInstance) = proc diff(newNode, oldNode: VNode;parent, current: Node; patches: JSeq[Patch]): EqResult = result = eq(newNode, oldNode) case result - of identical: + of identical, similar: newNode.dom = oldNode.dom + if result == similar: updateStyles(newNode, oldNode) + let newLength = newNode.len var oldLength = oldNode.len let minLength = min(newLength, oldLength) @@ -306,8 +308,6 @@ proc diff(newNode, oldNode: VNode;parent, current: Node; patches: JSeq[Patch]): patches.addPatch(pkRemove, current, current.childNodes[i], nil) result = different - of similar: - updateStyles(newNode, oldNode) of changed: assert oldNode.kind == VNodeKind.component let x = VComponent(oldNode) From fe7d72fa4e6d8dfd1051b4e6bc159aacfa35f6ff Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 28 Jun 2017 08:46:48 +0200 Subject: [PATCH 071/247] special treatment for input and textarea fields; minor code cleanups --- src/karax.nim | 6 ++++-- src/vdom.nim | 1 - tests/blur.nim | 13 +++++-------- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index f8230c7..09a3e1b 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -52,7 +52,9 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind; action: EventHandler) = let action = action let n = n result = proc (ev: Event) = - action(ev, n) + if n.kind == VNodeKind.textarea or n.kind == VNodeKind.input: + keyeventBody() + else: action(ev, n) proc enterWrapper(): (proc (ev: Event)) = let action = action @@ -189,7 +191,7 @@ proc updateStyles(newNode, oldNode: VNode) = oldNode.style = newNode.style proc printV(n: VNode; depth: cstring = "") = - kout depth, cstring($n.kind), n.myid, cstring"key ", n.key + kout depth, cstring($n.kind), cstring"key ", n.key #for k, v in pairs(n.style): # kout depth, "style: ", k, v if n.kind == VNodeKind.component: diff --git a/src/vdom.nim b/src/vdom.nim index 9a88d0a..e3f74b5 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -120,7 +120,6 @@ type style*: VStyle ## the style that should be applied to the virtual node. dom*: Node ## the attached real DOM node. Can be 'nil' if the virtual node ## is not part of the virtual DOM anymore. - myid*: int VComponent* = ref object of VNode ## The abstract class for every karax component. renderImpl*: proc(self: VComponent): VNode diff --git a/tests/blur.nim b/tests/blur.nim index 74b8ba1..4ad3eb1 100644 --- a/tests/blur.nim +++ b/tests/blur.nim @@ -4,7 +4,6 @@ type TextInput* = ref object of VComponent value: cstring isActive: bool -var renderId: int proc render(x: VComponent): VNode = let self = TextInput(x) @@ -37,18 +36,16 @@ proc render(x: VComponent): VNode = self.isActive = not self.isActive markDirty(self) - kout cstring"rendering ", self.myid - inc renderId - result = buildHtml(tdiv(style=style)): - input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip) + proc onchanged(ev: Event; n: VNode) = + kout cstring"onchanged", n.value + + result = buildHtml(tdiv(style=style)): + textarea(style=inputStyle, value=self.value, onblur=flip, onfocus=flip, onkeyup=onchanged) -var gid = 0 proc newTextInput*(style: VStyle = VStyle(); value: cstring = cstring""): TextInput = result = newComponent(TextInput, render) result.style = style result.value = value - inc gid - result.myid = gid proc createDom(): VNode = result = buildHtml(tdiv): From c987323dbaac4770e1c733046372ecbf38a1e7df Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 28 Jun 2017 09:44:50 +0200 Subject: [PATCH 072/247] test nested components (still buggy) --- tests/blur.nim | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/blur.nim b/tests/blur.nim index 4ad3eb1..02a0de2 100644 --- a/tests/blur.nim +++ b/tests/blur.nim @@ -34,21 +34,37 @@ proc render(x: VComponent): VNode = proc flip(ev: Event; n: VNode) = self.isActive = not self.isActive + kout cstring"onflip", n.value markDirty(self) proc onchanged(ev: Event; n: VNode) = kout cstring"onchanged", n.value result = buildHtml(tdiv(style=style)): - textarea(style=inputStyle, value=self.value, onblur=flip, onfocus=flip, onkeyup=onchanged) + input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip, onkeyup=onchanged) proc newTextInput*(style: VStyle = VStyle(); value: cstring = cstring""): TextInput = result = newComponent(TextInput, render) result.style = style result.value = value +type + Combined = ref object of VComponent + a, b: TextInput + +proc renderComb(self: VComponent): VNode = + let self = Combined(self) + result = buildHtml(tdiv(style=self.style)): + self.a + self.b + +proc newCombined*(style: VStyle = VStyle()): Combined = + result = newComponent(Combined, renderComb) + result.a = newTextInput(style, "AAA") + result.b = newTextInput(style, "BBB") + proc createDom(): VNode = result = buildHtml(tdiv): - newTextInput(value=cstring"test") + newCombined() setRenderer createDom From ba88fb9a0b2e15c7eebf49498706b6a9c4ffdc49 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 28 Jun 2017 09:46:26 +0200 Subject: [PATCH 073/247] cancel redraw requests as an attempt to improve performance --- src/karax.nim | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 09a3e1b..0b1d8db 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -23,6 +23,7 @@ type postRenderCallback: proc () toFocus: Node toFocusV: VNode + renderId: int var kxi*: KaraxInstance ## The current Karax instance. This is always used @@ -354,7 +355,8 @@ proc dodraw(kxi: KaraxInstance) = if kxi.toFocus != nil: kxi.toFocus.focus() -proc reqFrame(callback: proc()) {.importc: "window.requestAnimationFrame".} +proc reqFrame(callback: proc()): int {.importc: "window.requestAnimationFrame".} +proc cancelFrame(id: int) {.importc: "window.cancelAnimationFrame".} proc redraw*(kxi: KaraxInstance = kxi) = # we buffer redraw requests: @@ -363,14 +365,15 @@ proc redraw*(kxi: KaraxInstance = kxi) = clearTimeout(drawTimeout) drawTimeout = setTimeout(dodraw, 30) elif true: - reqFrame(proc () = kxi.dodraw) + cancelFrame kxi.renderId + kxi.renderId = reqFrame(proc () = kxi.dodraw) else: dodraw(kxi) proc redrawSync*(kxi: KaraxInstance = kxi) = dodraw(kxi) proc init(ev: Event) = - reqFrame(proc () = kxi.dodraw) + kxi.renderId = reqFrame(proc () = kxi.dodraw) proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT", clientPostRenderCallback: proc () = nil): KaraxInstance {.discardable.} = From 0ad9d68c8102f336b4e721e30e06c40186e1880c Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 28 Jun 2017 11:26:09 +0200 Subject: [PATCH 074/247] fixes #35 --- src/karax.nim | 39 ++++++++++++++++++++++++++++++--------- src/kdom.nim | 1 + src/vdom.nim | 5 +++-- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 0b1d8db..5ea7d62 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -48,8 +48,9 @@ template keyeventBody() = # Do not call redraw() here! That is already done # by ``karax.addEventHandler``. -proc wrapEvent(d: Node; n: VNode; k: EventKind; action: EventHandler) = - proc stdWrapper(): (proc (ev: Event)) = +proc wrapEvent(d: Node; n: VNode; k: EventKind; + action: EventHandler): NativeEventHandler = + proc stdWrapper(): NativeEventHandler = let action = action let n = n result = proc (ev: Event) = @@ -57,13 +58,13 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind; action: EventHandler) = keyeventBody() else: action(ev, n) - proc enterWrapper(): (proc (ev: Event)) = + proc enterWrapper(): NativeEventHandler = let action = action let n = n result = proc (ev: Event) = if ev.keyCode == 13: keyeventBody() - proc laterWrapper(): (proc (ev: Event)) = + proc laterWrapper(): NativeEventHandler = let action = action let n = n var timer: Timeout @@ -74,11 +75,14 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind; action: EventHandler) = case k of EventKind.onkeyuplater: - d.addEventListener("keyup", laterWrapper()) + result = laterWrapper() + d.addEventListener("keyup", result) of EventKind.onkeyupenter: - d.addEventListener("keyup", enterWrapper()) + result = enterWrapper() + d.addEventListener("keyup", result) else: - d.addEventListener(toEventName[k], stdWrapper()) + result = stdWrapper() + d.addEventListener(toEventName[k], result) # --------------------- DOM diff ----------------------------------------- @@ -90,6 +94,11 @@ template detach(n: VNode) = template attach(n: VNode) = n.dom = result +proc applyEvents(n: VNode; kxi: KaraxInstance) = + let dest = n.dom + for i in 0.. Date: Wed, 28 Jun 2017 12:56:51 +0200 Subject: [PATCH 075/247] combined components work --- tests/blur.nim | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/blur.nim b/tests/blur.nim index 02a0de2..f40cb97 100644 --- a/tests/blur.nim +++ b/tests/blur.nim @@ -58,8 +58,12 @@ proc renderComb(self: VComponent): VNode = self.a self.b +proc changed(self: VComponent): bool = + let self = Combined(self) + result = self.a.changedImpl(self.a) or self.b.changedImpl(self.b) + proc newCombined*(style: VStyle = VStyle()): Combined = - result = newComponent(Combined, renderComb) + result = newComponent(Combined, renderComb, changed=changed) result.a = newTextInput(style, "AAA") result.b = newTextInput(style, "BBB") From c681f0055f2d63c7cee91788e65e8cb2acf7db9d Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 28 Jun 2017 15:23:19 +0200 Subject: [PATCH 076/247] update value property of real DOM nodes; minor DOM diff speedups; allow setFocus=false in the DSL --- src/jdict.nim | 2 +- src/karax.nim | 90 +++++++++++++++++++++++++++++------------------- src/karaxdsl.nim | 4 ++- tests/blur.nim | 9 +++++ 4 files changed, 67 insertions(+), 38 deletions(-) diff --git a/src/jdict.nim b/src/jdict.nim index b239af2..6e2aa9f 100644 --- a/src/jdict.nim +++ b/src/jdict.nim @@ -17,7 +17,7 @@ type proc `[]`*[T](s: JSeq[T], i: int): T {.importcpp: "#[#]", noSideEffect.} proc `[]=`*[T](s: JSeq[T], i: int, v: T) {.importcpp: "#[#] = #", noSideEffect.} -proc newJSeq*[T](): JSeq[T] {.importcpp: "[@]".} +proc newJSeq*[T](len: int = 0): JSeq[T] {.importcpp: "new Array(#)".} proc len*[T](s: JSeq[T]): int {.importcpp: "#.length", noSideEffect.} proc add*[T](s: JSeq[T]; x: T) {.importcpp: "#.push(#)", noSideEffect.} diff --git a/src/karax.nim b/src/karax.nim index 5ea7d62..ad8ddfc 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -14,6 +14,14 @@ proc key*(e: Node): VKey = else: result = -1 proc `key=`*(e: Node; x: VKey) {.importcpp: "#.karaxKey = #", nodecl.} +type + PatchKind = enum + pkReplace, pkRemove, pkAppend, pkInsertBefore + Patch = object + k: PatchKind + parent, current: Node + n: VNode + type KaraxInstance* = ref object ## underlying karax instance. Usually you don't have ## know about this. @@ -24,6 +32,9 @@ type toFocus: Node toFocusV: VNode renderId: int + patches: seq[Patch] # we reuse this to save allocations + patchLen: int + var kxi*: KaraxInstance ## The current Karax instance. This is always used @@ -33,8 +44,9 @@ var ## in your 'buildHtml' statement, it needs to be named ## 'kxi'. -proc setFocus*(n: VNode; kxi: KaraxInstance = kxi) = - kxi.toFocusV = n +proc setFocus*(n: VNode; enabled = true; kxi: KaraxInstance = kxi) = + if enabled: + kxi.toFocusV = n # ----------------- event wrapping --------------------------------------- @@ -199,7 +211,7 @@ proc updateStyles(newNode, oldNode: VNode) = else: oldNode.dom.style = Style() oldNode.style = newNode.style -proc mergeEvents(newNode, oldNode: VNode) = +proc mergeEvents(newNode, oldNode: VNode; kxi: KaraxInstance) = let d = oldNode.dom for i in 0..= kxi.patches.len: + # allocate more space: + 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].n = na + inc kxi.patchLen -proc addPatch(patches: JSeq[Patch]; k: PatchKind; parent, current: Node; - n: VNode) = - patches.add(Patch(k: k, parent: parent, current: current, n: n)) - -proc apply(patches: JSeq[Patch]; kxi: KaraxInstance) = - for i in 0.. Date: Wed, 28 Jun 2017 22:54:24 +0200 Subject: [PATCH 077/247] make setFocus: bool work --- examples/todoapp/todoapp.nim | 2 +- src/karaxdsl.nim | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/todoapp/todoapp.nim b/examples/todoapp/todoapp.nim index f74abc3..0caa70c 100644 --- a/examples/todoapp/todoapp.nim +++ b/examples/todoapp/todoapp.nim @@ -90,7 +90,7 @@ proc createEntry(id: int; d: cstring; completed, selected: bool): VNode {.compac else: input(class = "edit", name = "title", key=id, onblur = focusLost, - onkeyupenter = editEntry, value = d, setFocus) + onkeyupenter = editEntry, value = d, setFocus=true) proc makeFooter(entriesCount, completedCount: int): VNode {.compact.} = result = buildHtml(footer(class = "footer")): diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index 47cd761..15c6fc6 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -86,7 +86,7 @@ proc tcall2(n, tmpContext: NimNode): NimNode = elif key in SpecialAttrs: result.add newDotAsgn(tmp, key, x[1]) elif eqIdent(key, "setFocus"): - result.add newCall(x, tmp, x[1], ident"kxi") + result.add newCall(key, tmp, x[1], ident"kxi") else: result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1]) elif ck != ComponentKind.Tag: From e7399094667f3fb6d31477feefce7457050d67b4 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 29 Jun 2017 11:04:43 +0200 Subject: [PATCH 078/247] rollback dom.value behaviour; fixes removeEventListener problem --- src/karax.nim | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index ad8ddfc..595ff60 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -213,12 +213,13 @@ proc updateStyles(newNode, oldNode: VNode) = proc mergeEvents(newNode, oldNode: VNode; kxi: KaraxInstance) = let d = oldNode.dom - for i in 0.. Date: Thu, 29 Jun 2017 12:25:08 +0200 Subject: [PATCH 079/247] different detach handling to make event handlers work better --- src/karax.nim | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 595ff60..b8c2aa1 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -16,7 +16,7 @@ proc `key=`*(e: Node; x: VKey) {.importcpp: "#.karaxKey = #", nodecl.} type PatchKind = enum - pkReplace, pkRemove, pkAppend, pkInsertBefore + pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach Patch = object k: PatchKind parent, current: Node @@ -99,10 +99,8 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind; # --------------------- DOM diff ----------------------------------------- template detach(n: VNode) = - if n.kind == VNodeKind.component: - let x = VComponent(n) - if x.onDetachImpl != nil: x.onDetachImpl(x) - n.dom = nil + addPatch(kxi, pkDetach, nil, nil, n) + template attach(n: VNode) = n.dom = result @@ -213,13 +211,12 @@ proc updateStyles(newNode, oldNode: VNode) = proc mergeEvents(newNode, oldNode: VNode; kxi: KaraxInstance) = let d = oldNode.dom - if d != nil: - for i in 0..= kxi.patches.len: @@ -266,6 +263,12 @@ proc apply(kxi: KaraxInstance) = of pkInsertBefore: let nn = vnodeToDom(p.n, kxi) p.parent.insertBefore(nn, p.current) + of pkDetach: + let n = p.n + if n.kind == VNodeKind.component: + let x = VComponent(n) + if x.onDetachImpl != nil: x.onDetachImpl(x) + n.dom = nil kxi.patchLen = 0 proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): EqResult = @@ -273,6 +276,7 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E case result of identical, similar: newNode.dom = oldNode.dom + assert oldNode.dom != nil if result == similar: updateStyles(newNode, oldNode) if newNode.events.len != 0 or oldNode.events.len != 0: mergeEvents(newNode, oldNode, kxi) @@ -280,9 +284,9 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E # oldNode.dom.value = newNode.text let newLength = newNode.len - var oldLength = oldNode.len - let minLength = min(newLength, oldLength) + let oldLength = oldNode.len if newLength == 0 and oldLength == 0: return result + let minLength = min(newLength, oldLength) assert oldNode.kind == newNode.kind var commonPrefix = 0 From ea7eee756498452224bc822aeeb3d3a27829e4e8 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 29 Jun 2017 17:10:29 +0200 Subject: [PATCH 080/247] better input element handling --- src/karax.nim | 13 ++++++++----- tests/blur.nim | 43 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index b8c2aa1..3c41d7e 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -54,9 +54,11 @@ template nativeValue(ev): cstring = cast[Element](ev.target).value template setNativeValue(ev, val) = cast[Element](ev.target).value = val template keyeventBody() = - n.value = nativeValue(ev) + let v = nativeValue(ev) + n.value = v action(ev, n) - setNativeValue(ev, n.value) + if n.value != v: + setNativeValue(ev, n.value) # Do not call redraw() here! That is already done # by ``karax.addEventHandler``. @@ -276,12 +278,13 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E case result of identical, similar: newNode.dom = oldNode.dom - assert oldNode.dom != nil if result == similar: updateStyles(newNode, oldNode) if newNode.events.len != 0 or oldNode.events.len != 0: mergeEvents(newNode, oldNode, kxi) - #if newNode.kind == VNodeKind.input or newNode.kind == VNodeKind.textarea: - # oldNode.dom.value = newNode.text + 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 diff --git a/tests/blur.nim b/tests/blur.nim index 84d69dd..844ab8b 100644 --- a/tests/blur.nim +++ b/tests/blur.nim @@ -3,6 +3,7 @@ import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils type TextInput* = ref object of VComponent value: cstring isActive: bool + onchange: proc (value: cstring) proc render(x: VComponent): VNode = let self = TextInput(x) @@ -38,15 +39,23 @@ proc render(x: VComponent): VNode = markDirty(self) proc onchanged(ev: Event; n: VNode) = - kout cstring"onchanged", n.value + if self.onchange != nil and self.value != n.value: + self.onchange n.value + self.value = n.value result = buildHtml(tdiv(style=style)): input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip, onkeyup=onchanged) -proc newTextInput*(style: VStyle = VStyle(); value: cstring = cstring""): TextInput = +proc setValue(x: TextInput; value: cstring) = + x.value = value + markDirty(x) + +proc newTextInput*(style: VStyle = VStyle(); value: cstring = cstring"", + onchange: proc(v: cstring) = nil): TextInput = result = newComponent(TextInput, render) result.style = style result.value = value + result.onchange = onchange type Combined = ref object of VComponent @@ -76,8 +85,36 @@ proc newCombined*(style: VStyle = VStyle()): Combined = result.a = newTextInput(style, "AAA") result.b = newTextInput(style, "BBB") + +var + persons: seq[cstring] = @[cstring"Karax", "Abathur", "Fenix"] + selected = -1 + errmsg = cstring"" + ti = newTextInput(VStyle(), "", proc (v: cstring) = + if v.len > 0: + if selected >= 0: persons[selected] = v + errmsg = "" + else: + errmsg = "name must not be empty" + ) + +proc renderPerson(text: cstring, index: int): VNode = + proc select(ev: Event, n: VNode) = + selected = index + ti.setValue(persons[selected]) + + result = buildHtml(): + tdiv(onClick=select): + text text + proc createDom(): VNode = result = buildHtml(tdiv): - newCombined() + tdiv: + for index, text in persons.pairs: + renderPerson(text, index) + tdiv: + ti + tdiv: + text errmsg setRenderer createDom From d538b7bc938f8c70cf72980b9e5a7ae8905192e3 Mon Sep 17 00:00:00 2001 From: Araq Date: Sun, 2 Jul 2017 09:34:58 +0200 Subject: [PATCH 081/247] try to make components aware of the previous state --- src/karax.nim | 10 ++++++--- src/vdom.nim | 17 ++++++++------- tests/blur.nim | 56 +++++++++++++++++++++++++++++++------------------- 3 files changed, 50 insertions(+), 33 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 3c41d7e..5edda89 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -132,7 +132,8 @@ proc vnodeToDom(n: VNode; kxi: KaraxInstance): Node = assert x.renderImpl != nil if x.expanded == nil: x.expanded = x.renderImpl(x) - x.updatedImpl(x) + # x.updatedImpl(x, nil) + assert x.expanded != nil result = vnodeToDom(x.expanded, kxi) attach n return result @@ -197,7 +198,7 @@ proc eq(a, b: VNode): EqResult = if a.text != b.text: return different let x = VComponent(b) assert x.changedImpl != nil - return if x.changedImpl(x): changed else: identical + return if x.changedImpl(x, VComponent(a)): changed else: identical if not sameAttrs(a, b): return different if a.class != b.class: return different if not eq(a.style, b.style): return similar @@ -282,6 +283,7 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E if newNode.events.len != 0 or oldNode.events.len != 0: mergeEvents(newNode, oldNode, kxi) if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea: + echo "comparing ", oldNode.text != newNode.text if oldNode.text != newNode.text: oldNode.text = newNode.text oldNode.dom.value = newNode.text @@ -359,9 +361,11 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E of changed: assert oldNode.kind == VNodeKind.component let x = VComponent(oldNode) + x.updatedImpl(x, VComponent newNode) let oldExpanded = x.expanded x.expanded = x.renderImpl(x) - x.updatedImpl(x) + x.version = VComponent(newNode).version + #x.updatedImpl(x) if oldExpanded == nil: detach(oldNode) kxi.addPatch(pkReplace, parent, current, x.expanded) diff --git a/src/vdom.nim b/src/vdom.nim index b0f7293..bbb4eac 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -124,14 +124,13 @@ type VComponent* = ref object of VNode ## The abstract class for every karax component. renderImpl*: proc(self: VComponent): VNode - changedImpl*: proc(self: VComponent): bool - updatedImpl*: proc(self: VComponent) + changedImpl*: proc(self, newInstance: VComponent): bool + updatedImpl*: proc(self, newInstance: VComponent) onAttachImpl*: proc(self: VComponent) onDetachImpl*: proc(self: VComponent) version*: int ## Update this to trigger a redraw by karax. Usually you ## should call 'markDirty' instead which is an alias for ## 'inc version'. - renderedVersion*: int ## Do not touch. Used by karax. expanded*: VNode ## Do not touch. Used by karax. The VDOM the component ## expanded to. @@ -153,19 +152,19 @@ proc vthunk*(name: cstring; args: varargs[VNode, vn]): VNode = proc dthunk*(name: cstring; args: varargs[VNode, vn]): VNode = VNode(kind: VNodeKind.dthunk, text: name, key: -1, kids: @args) -proc defaultChangedImpl*(v: VComponent): bool = +proc defaultChangedImpl*(v, newInstance: VComponent): bool = ## The default implementation of 'changed'. - result = v.version != v.renderedVersion + result = v.version != newInstance.version -proc defaultUpdatedImpl*(v: VComponent) = - v.renderedVersion = v.version +proc defaultUpdatedImpl*(v, newInstance: VComponent) = + discard template newComponent*[T](t: typeDesc[T]; render: (proc(self: VComponent): VNode) not nil, onAttach: proc(self: VComponent) = nil, onDetach: proc(self: VComponent) = nil, - changed: (proc(self: VComponent): bool) = defaultChangedImpl, - updated: proc(self: VComponent) = defaultUpdatedImpl): T = + changed: (proc(self, newInstance: VComponent): bool) = defaultChangedImpl, + updated: proc(self, newInstance: VComponent) = defaultUpdatedImpl): T = ## Use this template to create new components. T(kind: VNodeKind.component, key: -1, text: cstring(astToStr(t)), renderImpl: render, diff --git a/tests/blur.nim b/tests/blur.nim index 844ab8b..3823f93 100644 --- a/tests/blur.nim +++ b/tests/blur.nim @@ -41,18 +41,33 @@ proc render(x: VComponent): VNode = proc onchanged(ev: Event; n: VNode) = if self.onchange != nil and self.value != n.value: self.onchange n.value - self.value = n.value + #self.value = n.value result = buildHtml(tdiv(style=style)): input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip, onkeyup=onchanged) -proc setValue(x: TextInput; value: cstring) = - x.value = value - markDirty(x) +#proc setValue(x: TextInput; value: cstring) = +# x.value = value +# markDirty(x) + +proc update(current, next: VComponent) = + let current = TextInput(current) + let next = TextInput(next) + if not current.isActive: + current.value = next.value + echo "updated! ", current.value + else: + echo "not updated! ", current.value + #markDirty(current) + +proc changed(current, next: VComponent): bool = + let current = TextInput(current) + let next = TextInput(next) + result = next.value != current.value or true proc newTextInput*(style: VStyle = VStyle(); value: cstring = cstring"", onchange: proc(v: cstring) = nil): TextInput = - result = newComponent(TextInput, render) + result = newComponent(TextInput, render, changed=changed, updated=update) result.style = style result.value = value result.onchange = onchange @@ -76,32 +91,25 @@ proc renderComb(self: VComponent): VNode = button(onclick=bu): text "reset" -proc changed(self: VComponent): bool = - let self = Combined(self) - result = self.a.changedImpl(self.a) or self.b.changedImpl(self.b) +when false: + proc changed(self: VComponent): bool = + let self = Combined(self) + result = self.a.changedImpl(self.a) or self.b.changedImpl(self.b) -proc newCombined*(style: VStyle = VStyle()): Combined = - result = newComponent(Combined, renderComb, changed=changed) - result.a = newTextInput(style, "AAA") - result.b = newTextInput(style, "BBB") + proc newCombined*(style: VStyle = VStyle()): Combined = + result = newComponent(Combined, renderComb, changed=changed) + result.a = newTextInput(style, "AAA") + result.b = newTextInput(style, "BBB") var persons: seq[cstring] = @[cstring"Karax", "Abathur", "Fenix"] selected = -1 errmsg = cstring"" - ti = newTextInput(VStyle(), "", proc (v: cstring) = - if v.len > 0: - if selected >= 0: persons[selected] = v - errmsg = "" - else: - errmsg = "name must not be empty" - ) proc renderPerson(text: cstring, index: int): VNode = proc select(ev: Event, n: VNode) = selected = index - ti.setValue(persons[selected]) result = buildHtml(): tdiv(onClick=select): @@ -113,7 +121,13 @@ proc createDom(): VNode = for index, text in persons.pairs: renderPerson(text, index) tdiv: - ti + newTextInput(VStyle(), if selected >= 0: persons[selected] else: "", proc (v: cstring) = + if v.len > 0: + if selected >= 0: persons[selected] = v + errmsg = "" + else: + errmsg = "name must not be empty" + ) tdiv: text errmsg From 43ee8903561b0d06adb26c1d88f6d8ecb4cd7929 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 3 Jul 2017 23:03:30 +0200 Subject: [PATCH 082/247] fix exponential blowup in diff algorith; wip --- src/karax.nim | 96 +++++++++++++++++++++++++++++++++------------- tests/difftest.nim | 37 +++++++++++++----- 2 files changed, 96 insertions(+), 37 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 3c41d7e..0be777a 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -34,6 +34,7 @@ type renderId: int patches: seq[Patch] # we reuse this to save allocations patchLen: int + recursion: int var @@ -161,6 +162,9 @@ proc vnodeToDom(n: VNode; kxi: KaraxInstance): Node = proc same(n: VNode, e: Node): bool = if n.kind == VNodeKind.component: result = same(VComponent(n).expanded, e) + elif n.kind == VNodeKind.vthunk or n.kind == VNodeKind.dthunk: + # we don't check these for now: + result = true elif toTag[n.kind] == e.nodename: result = true if n.kind != VNodeKind.text: @@ -178,7 +182,7 @@ proc replaceById(id: cstring; newTree: Node) = type EqResult = enum - changed, different, similar, identical + changed, different, similar, identical, usenewNode proc eq(a, b: VNode): EqResult = if a.kind != b.kind: return different @@ -274,6 +278,13 @@ proc apply(kxi: KaraxInstance) = kxi.patchLen = 0 proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): EqResult = + if kxi.recursion > 100: + 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 result = eq(newNode, oldNode) case result of identical, similar: @@ -293,68 +304,85 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E 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 - when false: - if oldNode.kind notin {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}: - assert current != nil - assert current.childNodes[j] != nil, $info - assert oldNode.len == current.len - - let r = if oldNode.kind == VNodeKind.component or oldNode.kind == VNodeKind.vthunk or - oldNode.kind == VNodeKind.dthunk: + 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) + (assert j < current.len; + diff(a[i], b[j], current, current.childNodes[j], kxi)) case r of identical, changed, similar: a[i] = b[j] action + of usenewNode: + b[j] = a[i] + action of different: # undo what 'diff' would have done: kxi.patchLen = oldLen - if result != different: result = r + #if result != different: result = r break - #of similar: - # updateStyles(a[i], b[j]) - # a[i] = b[j] - # action - + # compute common prefix: while commonPrefix < minLength: eqAndUpdate(newNode, commonPrefix, oldNode, commonPrefix, cstring"prefix"): inc commonPrefix + # compute common suffix: var oldPos = oldLength - 1 var newPos = newLength - 1 while oldPos >= commonPrefix and newPos >= commonPrefix: eqAndUpdate(newNode, newPos, oldNode, oldPos, cstring"suffix"): dec oldPos dec newPos + echo "came here" - var pos = min(oldPos, newPos) + 1 - for i in commonPrefix..pos-1: - if diff(newNode[i], oldNode[i], current, current.childNodes[i], - kxi) != different: - newNode[i] = oldNode[i] - else: - result = different + let pos = min(oldPos, newPos) + 1 + # now the different children are in commonPrefix .. pos - 1: + when false: + for i in commonPrefix..pos-1: + detach(oldNode[i]) + kxi.addPatch(pkReplace, current, current.childNodes[i], newNode[i]) + when true: + for i in commonPrefix..pos-1: + let r = diff(newNode[i], oldNode[i], current, current.childNodes[i], + kxi) + if r == usenewNode: + oldNode[i] = newNode[i] + elif r != different: + newNode[i] = oldNode[i] + #else: + # result = different if oldPos + 1 == oldLength: for i in pos..newPos: kxi.addPatch(pkAppend, current, nil, newNode[i]) - result = different + result = usenewNode + #result = different else: let before = current.childNodes[oldPos + 1] for i in pos..newPos: kxi.addPatch(pkInsertBefore, current, before, newNode[i]) - result = different + result = usenewNode + #result = different # 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) - result = different + result = usenewNode #different + # after the applied patch, conceptually the nodes are identical, so + # no further search is required. 'changed' needs to be propagated + # for the component system to work. 'similar' was transformed into + # identical too: + #if result == different or result == similar: + # result = identical of changed: assert oldNode.kind == VNodeKind.component @@ -375,6 +403,16 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E of different: detach(oldNode) kxi.addPatch(pkReplace, parent, current, newNode) + of usenewNode: doAssert(false, "eq returned usenewNode") + dec kxi.recursion + +when defined(stats): + proc depth(n: VNode; total: var int): int = + var m = 0 + for i in 0..= expected.len: + echo "patches differ; expected nothing but got: ", p + elif p != expected[i]: + echo "patches differ; expected ", expected[i], " but got: ", p #hasDom(kxi.currentTree) + kxi.patchLen = 0 proc testAppend() = let a = buildHtml(tdiv): @@ -26,7 +41,7 @@ proc testAppend() = li: text "A" li: text "B" li: text "C" - doDiff(a, b) + doDiff(a, b, "pkAppend li C") proc testInsert() = let a = buildHtml(tdiv): @@ -38,7 +53,7 @@ proc testInsert() = li: text "A" li: text "B" li: text "C" - doDiff(a, b) + doDiff(a, b, "pkInsert li B") proc testDelete() = let a = buildHtml(tdiv): @@ -49,10 +64,12 @@ proc testDelete() = let b = buildHtml(tdiv): ul: discard - doDiff(a, b) + doDiff(a, b, "pkDetach li A", "pkRemove nil", + "pkDetach li B", "pkRemove nil", + "pkDetach li C", "pkRemove nil") kxi = KaraxInstance(rootId: cstring"ROOT", renderer: proc (): VNode = discard) -testAppend() +#testAppend() testInsert() -testDelete() +#testDelete() From 5f7f33bb0232810cd9f9edd9454caa7230814e81 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Tue, 4 Jul 2017 08:04:04 +0200 Subject: [PATCH 083/247] wip --- src/karax.nim | 7 +++---- tests/difftest.nim | 33 +++++++++++++++++++++++++++++---- tests/tester.nim | 1 + 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 0be777a..147965c 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -282,7 +282,7 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode) if oldNode.kind == VNodeKind.text: echo oldNode.text - return + #return #doAssert false, "overflow!" inc kxi.recursion result = eq(newNode, oldNode) @@ -315,7 +315,7 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E let r = if isSpecial: diff(a[i], b[j], parent, current, kxi) else: - (assert j < current.len; + (assert same(b, current); #(j < current.len, "j: " & $j & " len " & $current.len); diff(a[i], b[j], current, current.childNodes[j], kxi)) case r of identical, changed, similar: @@ -327,7 +327,7 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E of different: # undo what 'diff' would have done: kxi.patchLen = oldLen - #if result != different: result = r + if result != different: result = r break # compute common prefix: while commonPrefix < minLength: @@ -341,7 +341,6 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E eqAndUpdate(newNode, newPos, oldNode, oldPos, cstring"suffix"): dec oldPos dec newPos - echo "came here" let pos = min(oldPos, newPos) + 1 # now the different children are in commonPrefix .. pos - 1: diff --git a/tests/difftest.nim b/tests/difftest.nim index 52b354f..8bd4d0f 100644 --- a/tests/difftest.nim +++ b/tests/difftest.nim @@ -19,15 +19,18 @@ proc shortRepr(n: VNode): string = for i in 0..= expected.len: echo "patches differ; expected nothing but got: ", p + inc err elif p != expected[i]: echo "patches differ; expected ", expected[i], " but got: ", p + inc err #hasDom(kxi.currentTree) kxi.patchLen = 0 @@ -53,7 +56,24 @@ proc testInsert() = li: text "A" li: text "B" li: text "C" - doDiff(a, b, "pkInsert li B") + doDiff(a, b, "pkInsertBefore li B") + +proc testInsert2() = + let a = buildHtml(tdiv): + tdiv: + tdiv: + ul: + li: text "A" + li: text "D" + let b = buildHtml(tdiv): + tdiv: + tdiv: + ul: + li: text "A" + li: text "B" + li: text "C" + li: text "D" + doDiff(a, b, "pkInsertBefore li B", "pkInsertBefore li C") proc testDelete() = let a = buildHtml(tdiv): @@ -70,6 +90,11 @@ proc testDelete() = kxi = KaraxInstance(rootId: cstring"ROOT", renderer: proc (): VNode = discard) -#testAppend() +testAppend() testInsert() -#testDelete() +testInsert2() +testDelete() +if err == 0: + echo "Success" +else: + quit 1 diff --git a/tests/tester.nim b/tests/tester.nim index 4f2ae66..60a787b 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -13,5 +13,6 @@ proc main = exec("nim js examples/scrollapp/scrollapp.nim") exec("nim js examples/mediaplayer/playerapp.nim") exec("nim js examples/carousel/carousel.nim") + exec("nim js -d:nodejs -r tests/difftest.nim") main() From a93e739081fbb3572777665165343854c394e5b5 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 5 Jul 2017 16:07:52 +0200 Subject: [PATCH 084/247] new component system: blur.nim example now works --- src/karax.nim | 5 ++-- src/vdom.nim | 13 +++++++-- tests/blur.nim | 75 ++++++++++++++++++++++++-------------------------- 3 files changed, 49 insertions(+), 44 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 5edda89..3fb0fc4 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -283,7 +283,6 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E if newNode.events.len != 0 or oldNode.events.len != 0: mergeEvents(newNode, oldNode, kxi) if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea: - echo "comparing ", oldNode.text != newNode.text if oldNode.text != newNode.text: oldNode.text = newNode.text oldNode.dom.value = newNode.text @@ -364,8 +363,8 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E x.updatedImpl(x, VComponent newNode) let oldExpanded = x.expanded x.expanded = x.renderImpl(x) - x.version = VComponent(newNode).version - #x.updatedImpl(x) + x.renderedVersion = x.version + #echo "expanding ", x.debugId if oldExpanded == nil: detach(oldNode) kxi.addPatch(pkReplace, parent, current, x.expanded) diff --git a/src/vdom.nim b/src/vdom.nim index bbb4eac..997b059 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -131,8 +131,11 @@ type version*: int ## Update this to trigger a redraw by karax. Usually you ## should call 'markDirty' instead which is an alias for ## 'inc version'. + renderedVersion*: int ## Do not touch. Used by karax. The last version of the + ## component we rendered. expanded*: VNode ## Do not touch. Used by karax. The VDOM the component ## expanded to. + debugId*: int proc value*(n: VNode): cstring = n.text proc `value=`*(n: VNode; v: cstring) = n.text = v @@ -154,11 +157,16 @@ proc dthunk*(name: cstring; args: varargs[VNode, vn]): VNode = proc defaultChangedImpl*(v, newInstance: VComponent): bool = ## The default implementation of 'changed'. - result = v.version != newInstance.version + result = v.version != v.renderedVersion proc defaultUpdatedImpl*(v, newInstance: VComponent) = discard +var gid = 0 +proc getDebugId(): int = + inc(gid) + gid + template newComponent*[T](t: typeDesc[T]; render: (proc(self: VComponent): VNode) not nil, onAttach: proc(self: VComponent) = nil, @@ -169,7 +177,8 @@ template newComponent*[T](t: typeDesc[T]; T(kind: VNodeKind.component, key: -1, text: cstring(astToStr(t)), renderImpl: render, changedImpl: changed, updatedImpl: updated, - onAttachImpl: onAttach, onDetachImpl: onDetach) + onAttachImpl: onAttach, onDetachImpl: onDetach, + debugId: getDebugId()) template markDirty*(c: VComponent) = ## mark the component as dirty so that it is re-rendered. diff --git a/tests/blur.nim b/tests/blur.nim index 3823f93..25c0b24 100644 --- a/tests/blur.nim +++ b/tests/blur.nim @@ -1,7 +1,7 @@ import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils type TextInput* = ref object of VComponent - value: cstring + value, guid: cstring isActive: bool onchange: proc (value: cstring) @@ -35,63 +35,60 @@ proc render(x: VComponent): VNode = proc flip(ev: Event; n: VNode) = self.isActive = not self.isActive - kout cstring"onflip", n.value + echo "flip! ", self.isActive, " id: ", self.debugId, " version ", self.version markDirty(self) proc onchanged(ev: Event; n: VNode) = - if self.onchange != nil and self.value != n.value: + if self.onchange != nil: self.onchange n.value - #self.value = n.value + self.value = n.value result = buildHtml(tdiv(style=style)): input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip, onkeyup=onchanged) -#proc setValue(x: TextInput; value: cstring) = -# x.value = value -# markDirty(x) +proc changed(current, next: VComponent): bool = + let current = TextInput(current) + let next = TextInput(next) + if current.guid != next.guid: + result = true + else: + result = defaultChangedImpl(current, next) proc update(current, next: VComponent) = let current = TextInput(current) let next = TextInput(next) - if not current.isActive: - current.value = next.value - echo "updated! ", current.value - else: - echo "not updated! ", current.value - #markDirty(current) + current.value = next.value + current.guid = next.guid + next.isActive = current.isActive -proc changed(current, next: VComponent): bool = - let current = TextInput(current) - let next = TextInput(next) - result = next.value != current.value or true - -proc newTextInput*(style: VStyle = VStyle(); value: cstring = cstring"", +proc newTextInput*(style: VStyle = VStyle(); guid: cstring; value: cstring = cstring"", onchange: proc(v: cstring) = nil): TextInput = result = newComponent(TextInput, render, changed=changed, updated=update) result.style = style result.value = value result.onchange = onchange - -type - Combined = ref object of VComponent - a, b: TextInput - -proc renderComb(self: VComponent): VNode = - let self = Combined(self) - - proc bu(ev: Event; n: VNode) = - self.a.value = "" - self.b.value = "" - markDirty(self.a) - markDirty(self.b) - - result = buildHtml(tdiv(style=self.style)): - self.a - self.b - button(onclick=bu): - text "reset" + result.guid = guid when false: + type + Combined = ref object of VComponent + a, b: TextInput + + proc renderComb(self: VComponent): VNode = + let self = Combined(self) + + proc bu(ev: Event; n: VNode) = + self.a.value = "" + self.b.value = "" + markDirty(self.a) + markDirty(self.b) + + result = buildHtml(tdiv(style=self.style)): + self.a + self.b + button(onclick=bu): + text "reset" + proc changed(self: VComponent): bool = let self = Combined(self) result = self.a.changedImpl(self.a) or self.b.changedImpl(self.b) @@ -121,7 +118,7 @@ proc createDom(): VNode = for index, text in persons.pairs: renderPerson(text, index) tdiv: - newTextInput(VStyle(), if selected >= 0: persons[selected] else: "", proc (v: cstring) = + newTextInput(VStyle(), &selected, if selected >= 0: persons[selected] else: "", proc (v: cstring) = if v.len > 0: if selected >= 0: persons[selected] = v errmsg = "" From 7779560b6b0002bae211dfe78d0310a683773fcb Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 6 Jul 2017 18:34:32 +0200 Subject: [PATCH 085/247] more tests and diff fixes; wip --- src/karax.nim | 55 +++++++++++++++++++++++-------- tests/diffDomTests.nim | 1 - tests/difftest.nim | 74 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 114 insertions(+), 16 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 147965c..adbd2f2 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -21,6 +21,9 @@ type k: PatchKind parent, current: Node n: VNode + PatchV = object + parent, newChild: VNode + pos: int type KaraxInstance* = ref object ## underlying karax instance. Usually you don't have @@ -34,7 +37,11 @@ type renderId: int patches: seq[Patch] # we reuse this to save allocations patchLen: int - recursion: int + patchesV: seq[PatchV] + patchLenV: int + runCount: int + when defined(stats): + recursion: int var @@ -251,6 +258,17 @@ proc addPatch(kxi: KaraxInstance; ka: PatchKind; parenta, currenta: Node; kxi.patches[L].n = na inc kxi.patchLen +proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) = + let L = kxi.patchLenV + if L >= kxi.patchesV.len: + # allocate more space: + kxi.patchesV.add(PatchV(parent: parent, newChild: newChild, pos: pos)) + else: + kxi.patchesV[L].parent = parent + kxi.patchesV[L].newChild = newChild + kxi.patchesV[L].pos = pos + inc kxi.patchLenV + proc apply(kxi: KaraxInstance) = for i in 0.. 100: - 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 + when defined(stats): + if kxi.recursion > 100: + 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 result = eq(newNode, oldNode) case result of identical, similar: @@ -310,6 +333,7 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E 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: @@ -322,11 +346,13 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E a[i] = b[j] action of usenewNode: - b[j] = a[i] + #b[j] = a[i] + kxi.addPatchV(b, j, a[i]) action of different: # undo what 'diff' would have done: kxi.patchLen = oldLen + kxi.patchLenV = oldLenV if result != different: result = r break # compute common prefix: @@ -403,7 +429,8 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E detach(oldNode) kxi.addPatch(pkReplace, parent, current, newNode) of usenewNode: doAssert(false, "eq returned usenewNode") - dec kxi.recursion + when defined(stats): + dec kxi.recursion when defined(stats): proc depth(n: VNode; total: var int): int = @@ -416,6 +443,7 @@ when defined(stats): proc dodraw(kxi: KaraxInstance) = if kxi.renderer.isNil: return let newtree = kxi.renderer() + inc kxi.runCount newtree.id = kxi.rootId kxi.toFocus = nil if kxi.currentTree == nil: @@ -428,7 +456,7 @@ proc dodraw(kxi: KaraxInstance) = #kout cstring"patch len ", patches.len apply(kxi) kxi.currentTree = newtree - doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) + #doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) if not kxi.postRenderCallback.isNil: kxi.postRenderCallback() @@ -437,8 +465,8 @@ proc dodraw(kxi: KaraxInstance) = if kxi.toFocus != nil: kxi.toFocus.focus() kxi.renderId = 0 - kxi.recursion = 0 when defined(stats): + kxi.recursion = 0 var total = 0 echo "depth ", depth(kxi.currentTree, total), " total ", total @@ -467,7 +495,8 @@ proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT", ## Setup Karax. Usually the return value can be ignored. result = KaraxInstance(rootId: root, renderer: renderer, postRenderCallback: clientPostRenderCallback, - patches: newSeq[Patch](60)) + patches: newSeq[Patch](60), + patchesV: newSeq[PatchV](30)) kxi = result window.onload = init diff --git a/tests/diffDomTests.nim b/tests/diffDomTests.nim index e28c6f6..56f8b9f 100644 --- a/tests/diffDomTests.nim +++ b/tests/diffDomTests.nim @@ -26,7 +26,6 @@ proc check(name: cstring; order: seq[int]) = let result = checkOrder(order) results.add name & (if result: cstring" - OK" else: cstring" -FAIL") -# result: 0 1 2 3 4 7 5 proc test1() = results.add cstring"test1 started" entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")] diff --git a/tests/difftest.nim b/tests/difftest.nim index 8bd4d0f..3fbb800 100644 --- a/tests/difftest.nim +++ b/tests/difftest.nim @@ -43,8 +43,10 @@ proc testAppend() = ul: li: text "A" li: text "B" - li: text "C" - doDiff(a, b, "pkAppend li C") + li: + tdiv: + text "C" + doDiff(a, b, "pkAppend li div C") proc testInsert() = let a = buildHtml(tdiv): @@ -88,12 +90,80 @@ proc testDelete() = "pkDetach li B", "pkRemove nil", "pkDetach li C", "pkRemove nil") +proc testDeleteMiddle() = + let a = buildHtml(tdiv): + ul: + li: + tdiv: text "A" + li: + tdiv: text "B" + li: + tdiv: text "C" + li: + tdiv: text "D" + li: + tdiv: text "E" + li: + tdiv: text "F" + li: + tdiv: text "G" + li: + tdiv: text "H" + let b = buildHtml(tdiv): + ul: + li: + tdiv: text "A" + li: + tdiv: text "B" + li: + tdiv: text "C" + li: + tdiv: text "D" + li: + tdiv: text "E" + li: + tdiv: text "F" + li: + tdiv: text "H" + doDiff(a, b, "pkDetach li div G", "pkRemove nil") + +proc createEntry(id: cstring): VNode = + result = buildHtml(): + button(id="" & id): + text id + +proc createEntries(entries: seq[cstring]): VNode = + result = buildHtml(tdiv()): + ul(id="ul"): + for e in entries: + createEntry(e) + for r in entries: + tdiv: + text r + +proc testWild() = + var entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring"7", cstring("5")] + let a = createEntries(entries) + entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")] + let b = createEntries(entries) + doDiff(a, b, "pkDetach button 7", "pkRemove nil", "pkDetach div 7", "pkRemove nil") + +proc testWildInsert() = + var entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")] + let a = createEntries(entries) + entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring"7", cstring("5")] + let b = createEntries(entries) + doDiff(a, b, "pkInsertBefore button 7", "pkInsertBefore div 7") + kxi = KaraxInstance(rootId: cstring"ROOT", renderer: proc (): VNode = discard) testAppend() testInsert() testInsert2() testDelete() +testWild() +testWildInsert() +testDeleteMiddle() if err == 0: echo "Success" else: From 907c372c27b019b0f3327cb084d680754d79cc96 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 6 Jul 2017 18:53:07 +0200 Subject: [PATCH 086/247] code cleanups --- src/karax.nim | 40 +++++++++++++--------------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index adbd2f2..b83f8c0 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -339,14 +339,12 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E let r = if isSpecial: diff(a[i], b[j], parent, current, kxi) else: - (assert same(b, current); #(j < current.len, "j: " & $j & " len " & $current.len); - diff(a[i], b[j], current, current.childNodes[j], kxi)) + diff(a[i], b[j], current, current.childNodes[j], kxi) case r of identical, changed, similar: a[i] = b[j] action of usenewNode: - #b[j] = a[i] kxi.addPatchV(b, j, a[i]) action of different: @@ -370,44 +368,32 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E let pos = min(oldPos, newPos) + 1 # now the different children are in commonPrefix .. pos - 1: - when false: - for i in commonPrefix..pos-1: - detach(oldNode[i]) - kxi.addPatch(pkReplace, current, current.childNodes[i], newNode[i]) - when true: - for i in commonPrefix..pos-1: - let r = diff(newNode[i], oldNode[i], current, current.childNodes[i], - kxi) - if r == usenewNode: - oldNode[i] = newNode[i] - elif r != different: - newNode[i] = oldNode[i] - #else: - # result = different + for i in commonPrefix..pos-1: + 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 = different if oldPos + 1 == oldLength: for i in pos..newPos: kxi.addPatch(pkAppend, current, nil, newNode[i]) result = usenewNode - #result = different else: let before = current.childNodes[oldPos + 1] for i in pos..newPos: kxi.addPatch(pkInsertBefore, current, before, newNode[i]) result = usenewNode - #result = different # 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) - result = usenewNode #different - # after the applied patch, conceptually the nodes are identical, so - # no further search is required. 'changed' needs to be propagated - # for the component system to work. 'similar' was transformed into - # identical too: - #if result == different or result == similar: - # result = identical + result = usenewNode of changed: assert oldNode.kind == VNodeKind.component @@ -456,7 +442,7 @@ proc dodraw(kxi: KaraxInstance) = #kout cstring"patch len ", patches.len apply(kxi) kxi.currentTree = newtree - #doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) + doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) if not kxi.postRenderCallback.isNil: kxi.postRenderCallback() From 920bd2db9d6e3432bcfe2dd4ae020ac40fa59d90 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Fri, 7 Jul 2017 16:40:48 +0200 Subject: [PATCH 087/247] bugfix: component handling --- src/karax.nim | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index a2ef499..ba992d9 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -167,9 +167,9 @@ proc vnodeToDom(n: VNode; kxi: KaraxInstance): Node = kxi.toFocus = result if not n.style.isNil: applyStyle(result, n.style) -proc same(n: VNode, e: Node): bool = +proc same(n: VNode, e: Node; nesting = 0): bool = if n.kind == VNodeKind.component: - result = same(VComponent(n).expanded, e) + result = same(VComponent(n).expanded, e, nesting+1) elif n.kind == VNodeKind.vthunk or n.kind == VNodeKind.dthunk: # we don't check these for now: result = true @@ -177,10 +177,10 @@ proc same(n: VNode, e: Node): bool = result = true if n.kind != VNodeKind.text: if e.len != n.len: - kout e.len, n.len + kout e.len, n.len, toTag[n.kind], nesting return false for i in 0 ..< n.len: - if not same(n[i], e[i]): return false + if not same(n[i], e[i], nesting+1): return false else: kout toTag[n.kind], e.nodename @@ -298,6 +298,7 @@ proc apply(kxi: KaraxInstance) = for i in 0.. Date: Fri, 7 Jul 2017 17:37:29 +0200 Subject: [PATCH 088/247] make todoapp work again --- src/karax.nim | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index ba992d9..c35e72a 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -311,7 +311,6 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E #doAssert false, "overflow!" inc kxi.recursion result = eq(newNode, oldNode) - assert(same(oldNode, current)) case result of identical, similar: newNode.dom = oldNode.dom @@ -350,6 +349,8 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E 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 @@ -372,8 +373,7 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E let pos = min(oldPos, newPos) + 1 # now the different children are in commonPrefix .. pos - 1: for i in commonPrefix..pos-1: - let r = 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]) From ec4a413a7bcbcc28c22f8faa26cbce9cb0200e07 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 10 Jul 2017 18:00:35 +0200 Subject: [PATCH 089/247] bugfix: better diff'ing for complex components --- src/karax.nim | 5 ++--- tests/blur.nim | 6 ++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index c35e72a..13c52a2 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -411,9 +411,8 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E else: let res = diff(x.expanded, oldExpanded, parent, current, kxi) if res == usenewNode: - #oldNode[i] = newNode[i] - #kxi.addPatchV(oldNode, i, newNode[i]) - kxi.addPatch(pkReplace, parent, current, x.expanded) + #kxi.addPatch(pkReplace, parent, current, x.expanded) + discard "diff created a patchset for us, so this is fine" elif res != different: x.expanded = oldExpanded assert oldExpanded.dom != nil, "old expanded.dom is nil" diff --git a/tests/blur.nim b/tests/blur.nim index 25c0b24..fbb0a46 100644 --- a/tests/blur.nim +++ b/tests/blur.nim @@ -59,7 +59,7 @@ proc update(current, next: VComponent) = let next = TextInput(next) current.value = next.value current.guid = next.guid - next.isActive = current.isActive + #next.isActive = current.isActive proc newTextInput*(style: VStyle = VStyle(); guid: cstring; value: cstring = cstring"", onchange: proc(v: cstring) = nil): TextInput = @@ -107,10 +107,12 @@ var proc renderPerson(text: cstring, index: int): VNode = proc select(ev: Event, n: VNode) = selected = index + errmsg = "" result = buildHtml(): - tdiv(onClick=select): + tdiv: text text + button(onClick = select) proc createDom(): VNode = result = buildHtml(tdiv): From 6c152ed516b2154fc16b26248676be9aa5601533 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 10 Jul 2017 21:42:22 +0200 Subject: [PATCH 090/247] smart attribute comparisons --- src/karax.nim | 19 ++++++++++++++++--- src/vdom.nim | 3 +++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 13c52a2..2035f19 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -210,9 +210,8 @@ proc eq(a, b: VNode): EqResult = let x = VComponent(b) assert x.changedImpl != nil return if x.changedImpl(x, VComponent(a)): changed else: identical - if not sameAttrs(a, b): return different if a.class != b.class: return different - if not eq(a.style, b.style): return similar + if not eq(a.style, b.style) or not sameAttrs(a, b): return similar # Do not test event listeners here! return result @@ -223,6 +222,16 @@ proc updateStyles(newNode, oldNode: VNode) = else: oldNode.dom.style = Style() oldNode.style = newNode.style +proc updateAttributes(newNode, oldNode: VNode) = + # we keep the oldNode, but take over the attributes from the new node: + if oldNode.dom != nil: + for k, _ in attrs(oldNode): + oldNode.dom.removeAttribute(k) + for k, v in attrs(newNode): + if v != nil: + oldNode.dom.setAttr(k, v) + takeOverAttr(newNode, oldNode) + proc mergeEvents(newNode, oldNode: VNode; kxi: KaraxInstance) = let d = oldNode.dom for i in 0.. 100: @@ -314,7 +325,9 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E case result of identical, similar: newNode.dom = oldNode.dom - if result == similar: updateStyles(newNode, oldNode) + if result == similar: + updateStyles(newNode, oldNode) + updateAttributes(newNode, oldNode) if newNode.events.len != 0 or oldNode.events.len != 0: mergeEvents(newNode, oldNode, kxi) if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea: diff --git a/src/vdom.nim b/src/vdom.nim index 997b059..f751676 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -199,6 +199,9 @@ proc getAttr*(n: VNode; key: cstring): cstring = for i in countup(0, n.attrs.len-2, 2): if n.attrs[i] == key: return n.attrs[i+1] +proc takeOverAttr*(newNode, oldNode: VNode) = + oldNode.attrs = newNode.attrs + proc len*(x: VNode): int = x.kids.len proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx] proc `[]=`*(x: VNode; idx: int; y: VNode) = x.kids[idx] = y From 1e378b2ac238754a0fab34f8e351f4df80608fdd Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Tue, 11 Jul 2017 00:20:43 +0200 Subject: [PATCH 091/247] separate component updating from diffing algorithm --- src/karax.nim | 87 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 52 insertions(+), 35 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 2035f19..b8b1d60 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -24,6 +24,9 @@ type PatchV = object parent, newChild: VNode pos: int + ComponentPair = object + oldNode, newNode: VComponent + parent, current: Node type KaraxInstance* = ref object ## underlying karax instance. Usually you don't have @@ -40,6 +43,7 @@ type patchesV: seq[PatchV] patchLenV: int runCount: int + components: seq[ComponentPair] when defined(stats): recursion: int @@ -190,7 +194,7 @@ proc replaceById(id: cstring; newTree: Node) = type EqResult = enum - changed, different, similar, identical, usenewNode + componentsIdentical, different, similar, identical, usenewNode proc eq(a, b: VNode): EqResult = if a.kind != b.kind: return different @@ -207,9 +211,7 @@ proc eq(a, b: VNode): EqResult = elif b.kind == VNodeKind.component: # different component names mean different components: if a.text != b.text: return different - let x = VComponent(b) - assert x.changedImpl != nil - return if x.changedImpl(x, VComponent(a)): changed else: identical + return componentsIdentical if a.class != b.class: return different if not eq(a.style, b.style) or not sameAttrs(a, b): return similar # Do not test event listeners here! @@ -279,7 +281,7 @@ proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) = kxi.patchesV[L].pos = pos inc kxi.patchLenV -proc apply(kxi: KaraxInstance) = +proc applyPatch(kxi: KaraxInstance) = for i in 0.. 100: @@ -323,11 +323,16 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E inc kxi.recursion result = eq(newNode, oldNode) case result - of identical, similar: + of identical, componentsIdentical, similar: newNode.dom = oldNode.dom if result == similar: updateStyles(newNode, oldNode) updateAttributes(newNode, oldNode) + elif result == componentsIdentical: + kxi.components.add ComponentPair(oldNode: VComponent(oldNode), + newNode: VComponent(newNode), + parent: parent, + current: current) if newNode.events.len != 0 or oldNode.events.len != 0: mergeEvents(newNode, oldNode, kxi) if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea: @@ -356,7 +361,7 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E else: diff(a[i], b[j], current, current.childNodes[j], kxi) case r - of identical, changed, similar: + of identical, componentsIdentical, similar: a[i] = b[j] action of usenewNode: @@ -410,27 +415,6 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E #doAssert i < current.childNodes.len kxi.addPatch(pkRemove, current, current.childNodes[i], nil) result = usenewNode - - of changed: - assert oldNode.kind == VNodeKind.component - let x = VComponent(oldNode) - x.updatedImpl(x, VComponent newNode) - let oldExpanded = x.expanded - x.expanded = x.renderImpl(x) - x.renderedVersion = x.version - if oldExpanded == nil: - detach(oldNode) - kxi.addPatch(pkReplace, parent, current, x.expanded) - else: - let res = diff(x.expanded, oldExpanded, parent, current, kxi) - if res == usenewNode: - #kxi.addPatch(pkReplace, parent, current, x.expanded) - discard "diff created a patchset for us, so this is fine" - elif res != different: - x.expanded = oldExpanded - assert oldExpanded.dom != nil, "old expanded.dom is nil" - else: - assert x.expanded.dom != nil, "expanded.dom is nil" of different: detach(oldNode) kxi.addPatch(pkReplace, parent, current, newNode) @@ -438,6 +422,38 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E when defined(stats): dec kxi.recursion +proc applyComponents(kxi: KaraxInstance) = + # the first 'diff' pass detects components in the VDOM. The + # 'applyComponents' expands components and so on until no + # components are left to check. + var i = 0 + # beware: 'diff' appends to kxi.components! + # So this is actually a fixpoint iteration: + while i < kxi.components.len: + let x = kxi.components[i].oldNode + let newNode = kxi.components[i].newNode + if x.changedImpl != nil and x.changedImpl(x, newNode): + let current = kxi.components[i].current + let parent = kxi.components[i].parent + x.updatedImpl(x, newNode) + let oldExpanded = x.expanded + x.expanded = x.renderImpl(x) + x.renderedVersion = x.version + if oldExpanded == nil: + detach(x) + kxi.addPatch(pkReplace, parent, current, x.expanded) + else: + let res = diff(x.expanded, oldExpanded, parent, current, kxi) + if res == usenewNode: + discard "diff created a patchset for us, so this is fine" + elif res != different: + x.expanded = oldExpanded + assert oldExpanded.dom != nil, "old expanded.dom is nil" + else: + assert x.expanded.dom != nil, "expanded.dom is nil" + inc i + setLen(kxi.components, 0) + when defined(stats): proc depth(n: VNode; total: var int): int = var m = 0 @@ -453,16 +469,16 @@ proc dodraw(kxi: KaraxInstance) = newtree.id = kxi.rootId kxi.toFocus = nil if kxi.currentTree == nil: - kxi.currentTree = newtree - let asdom = vnodeToDom(kxi.currentTree, 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) discard diff(newtree, kxi.currentTree, nil, olddom, kxi) #kout cstring"patch len ", patches.len - apply(kxi) - kxi.currentTree = newtree + applyComponents(kxi) + applyPatch(kxi) + kxi.currentTree = newtree doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) if not kxi.postRenderCallback.isNil: @@ -503,7 +519,8 @@ proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT", result = KaraxInstance(rootId: root, renderer: renderer, postRenderCallback: clientPostRenderCallback, patches: newSeq[Patch](60), - patchesV: newSeq[PatchV](30)) + patchesV: newSeq[PatchV](30), + components: @[]) kxi = result window.onload = init From 5838271f8b6e2d9c1b7188db3f79fa5e807d13a7 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Tue, 11 Jul 2017 12:43:32 +0200 Subject: [PATCH 092/247] nicer indentation --- src/karax.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index b8b1d60..22ceceb 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -239,8 +239,8 @@ proc mergeEvents(newNode, oldNode: VNode; kxi: KaraxInstance) = for i in 0.. Date: Tue, 11 Jul 2017 12:54:53 +0200 Subject: [PATCH 093/247] renamed key to 'index'; all virtual components have a 'key' attribute --- examples/mediaplayer/mediaplayer.nim | 10 +++++----- examples/todoapp/todoapp.nim | 16 ++++++++-------- src/karax.nim | 24 +++++++++--------------- src/karaxdsl.nim | 2 +- src/vdom.nim | 26 ++++++++++++++------------ 5 files changed, 37 insertions(+), 41 deletions(-) diff --git a/examples/mediaplayer/mediaplayer.nim b/examples/mediaplayer/mediaplayer.nim index 3f970a8..7bc9c65 100644 --- a/examples/mediaplayer/mediaplayer.nim +++ b/examples/mediaplayer/mediaplayer.nim @@ -15,7 +15,7 @@ proc `width=`(n: Node, w: int) {.importcpp: "#.width = #".} proc mplayer*(id, resource: cstring): VNode {.compact.} = proc handler(ev: Event; n: VNode) = let myVideo = document.getElementById(id) - case n.key + case n.index of Play: if myVideo.paused: myVideo.play() @@ -27,13 +27,13 @@ proc mplayer*(id, resource: cstring): VNode {.compact.} = else: discard result = buildHtml(tdiv): - button(onclick=handler, key=Play): + button(onclick=handler, index=Play): text "Play/Pause" - button(onclick=handler, key=Big): + button(onclick=handler, index=Big): text "Big" - button(onclick=handler, key=Small): + button(onclick=handler, index=Small): text "Small" - button(onclick=handler, key=Normal): + button(onclick=handler, index=Normal): text "Normal" br() br() diff --git a/examples/todoapp/todoapp.nim b/examples/todoapp/todoapp.nim index 0caa70c..47ddeae 100644 --- a/examples/todoapp/todoapp.nim +++ b/examples/todoapp/todoapp.nim @@ -45,19 +45,19 @@ proc onTodoEnter(ev: Event; n: VNode) = n.value = "" proc removeHandler(ev: Event; n: VNode) = - updateEntry(n.key, cstring(nil), false) + updateEntry(n.index, cstring(nil), false) proc editHandler(ev: Event; n: VNode) = - selectedEntry = n.key + selectedEntry = n.index proc focusLost(ev: Event; n: VNode) = selectedEntry = -1 proc editEntry(ev: Event; n: VNode) = - setEntryContent(n.key, n.value) + setEntryContent(n.index, n.value) selectedEntry = -1 proc toggleEntry(ev: Event; n: VNode) = - let id = n.key + let id = n.index markAsCompleted(id, not isCompleted(id)) proc onAllDone(ev: Event; n: VNode) = @@ -83,12 +83,12 @@ proc createEntry(id: int; d: cstring; completed, selected: bool): VNode {.compac if not selected: tdiv(class = "view"): input(class = "toggle", `type` = "checkbox", checked = toChecked(completed), - onclick=toggleEntry, key=id) - label(onDblClick=editHandler, key=id): + onclick=toggleEntry, index=id) + label(onDblClick=editHandler, index=id): text d - button(class = "destroy", key=id, onclick=removeHandler) + button(class = "destroy", index=id, onclick=removeHandler) else: - input(class = "edit", name = "title", key=id, + input(class = "edit", name = "title", index=id, onblur = focusLost, onkeyupenter = editEntry, value = d, setFocus=true) diff --git a/src/karax.nim b/src/karax.nim index 22ceceb..c96901b 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -7,13 +7,6 @@ export kdom.Event proc kout*[T](x: T) {.importc: "console.log", varargs.} ## the preferred way of debugging karax applications. -proc hasProp(e: Node; prop: cstring): bool {.importcpp: "(#.hasOwnProperty(#))".} -proc rawkey(e: Node): VKey {.importcpp: "#.karaxKey", nodecl.} -proc key*(e: Node): VKey = - if e.hasProp"karaxKey": result = e.rawkey - else: result = -1 -proc `key=`*(e: Node; x: VKey) {.importcpp: "#.karaxKey = #", nodecl.} - type PatchKind = enum pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach @@ -200,7 +193,7 @@ proc eq(a, b: VNode): EqResult = if a.kind != b.kind: return different if a.id != b.id: return different result = identical - if a.key != b.key: return different + if a.index != b.index: return different if a.kind == VNodeKind.text: if a.text != b.text: return different elif a.kind == VNodeKind.vthunk or a.kind == VNodeKind.dthunk: @@ -211,6 +204,7 @@ proc eq(a, b: VNode): EqResult = elif b.kind == VNodeKind.component: # different component names mean different components: if a.text != b.text: return different + if VComponent(a).key != VComponent(b).key: return different return componentsIdentical if a.class != b.class: return different if not eq(a.style, b.style) or not sameAttrs(a, b): return similar @@ -246,7 +240,7 @@ proc mergeEvents(newNode, oldNode: VNode; kxi: KaraxInstance) = applyEvents(oldNode, kxi) proc printV(n: VNode; depth: cstring = "") = - kout depth, cstring($n.kind), cstring"key ", n.key + kout depth, cstring($n.kind), cstring"key ", n.index #for k, v in pairs(n.style): # kout depth, "style: ", k, v if n.kind == VNodeKind.component: @@ -323,16 +317,16 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E inc kxi.recursion result = eq(newNode, oldNode) case result - of identical, componentsIdentical, similar: + of componentsIdentical: + kxi.components.add ComponentPair(oldNode: VComponent(oldNode), + newNode: VComponent(newNode), + parent: parent, + current: current) + of identical, similar: newNode.dom = oldNode.dom if result == similar: updateStyles(newNode, oldNode) updateAttributes(newNode, oldNode) - elif result == componentsIdentical: - kxi.components.add ComponentPair(oldNode: VComponent(oldNode), - newNode: VComponent(newNode), - parent: parent, - current: current) if newNode.events.len != 0 or oldNode.events.len != 0: mergeEvents(newNode, oldNode, kxi) if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea: diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index 15c6fc6..c17d725 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -4,7 +4,7 @@ from strutils import startsWith, toLowerAscii const StmtContext = ["kout", "inc", "echo", "dec", "!"] - SpecialAttrs = ["id", "class", "value", "key", "style"] + SpecialAttrs = ["id", "class", "value", "index", "style"] proc getName(n: NimNode): string = case n.kind diff --git a/src/vdom.nim b/src/vdom.nim index f751676..8e4f983 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -105,11 +105,11 @@ buildLookupTables() type EventHandler* = proc (ev: Event; target: VNode) {.closure.} NativeEventHandler* = proc (ev: Event) {.closure.} - VKey* = int + VKey* = cstring VNode* = ref object of RootObj kind*: VNodeKind - key*: VKey + index*: int ## a generally useful 'index' id*, class*, text*: cstring kids: seq[VNode] # even index: key, odd index: value; done this way for memory efficiency: @@ -123,6 +123,8 @@ type ## is not part of the virtual DOM anymore. VComponent* = ref object of VNode ## The abstract class for every karax component. + key*: VKey ## key that determines if two components are + ## identical. renderImpl*: proc(self: VComponent): VNode changedImpl*: proc(self, newInstance: VComponent): bool updatedImpl*: proc(self, newInstance: VComponent) @@ -140,20 +142,20 @@ type proc value*(n: VNode): cstring = n.text proc `value=`*(n: VNode; v: cstring) = n.text = v -proc intValue*(n: VNode): int = n.key -proc vn*(i: int): VNode = VNode(kind: VNodeKind.int, key: i) -proc vn*(b: bool): VNode = VNode(kind: VNodeKind.int, key: ord(b)) -proc vn*(x: cstring): VNode = VNode(kind: VNodeKind.text, key: -1, text: x) +proc intValue*(n: VNode): int = n.index +proc vn*(i: int): VNode = VNode(kind: VNodeKind.int, index: i) +proc vn*(b: bool): VNode = VNode(kind: VNodeKind.int, index: ord(b)) +proc vn*(x: cstring): VNode = VNode(kind: VNodeKind.text, index: -1, text: x) template callThunk*(fn: typed; n: VNode): untyped = ## for internal usage only. fn(n.kids) proc vthunk*(name: cstring; args: varargs[VNode, vn]): VNode = - VNode(kind: VNodeKind.vthunk, text: name, key: -1, kids: @args) + VNode(kind: VNodeKind.vthunk, text: name, index: -1, kids: @args) proc dthunk*(name: cstring; args: varargs[VNode, vn]): VNode = - VNode(kind: VNodeKind.dthunk, text: name, key: -1, kids: @args) + VNode(kind: VNodeKind.dthunk, text: name, index: -1, kids: @args) proc defaultChangedImpl*(v, newInstance: VComponent): bool = ## The default implementation of 'changed'. @@ -174,7 +176,7 @@ template newComponent*[T](t: typeDesc[T]; changed: (proc(self, newInstance: VComponent): bool) = defaultChangedImpl, updated: proc(self, newInstance: VComponent) = defaultUpdatedImpl): T = ## Use this template to create new components. - T(kind: VNodeKind.component, key: -1, + T(kind: VNodeKind.component, index: -1, text: cstring(astToStr(t)), renderImpl: render, changedImpl: changed, updatedImpl: updated, onAttachImpl: onAttach, onDetachImpl: onDetach, @@ -206,7 +208,7 @@ proc len*(x: VNode): int = x.kids.len proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx] proc `[]=`*(x: VNode; idx: int; y: VNode) = x.kids[idx] = y proc add*(parent, kid: VNode) = parent.kids.add kid -proc newVNode*(kind: VNodeKind): VNode = VNode(kind: kind, key: -1) +proc newVNode*(kind: VNodeKind): VNode = VNode(kind: kind, index: -1) proc tree*(kind: VNodeKind; kids: varargs[VNode]): VNode = result = newVNode(kind) @@ -217,8 +219,8 @@ proc tree*(kind: VNodeKind; attrs: openarray[(cstring, cstring)]; result = tree(kind, kids) for a in attrs: result.setAttr(a[0], a[1]) -proc text*(s: string): VNode = VNode(kind: VNodeKind.text, text: cstring(s), key: -1) -proc text*(s: cstring): VNode = VNode(kind: VNodeKind.text, text: s, key: -1) +proc text*(s: string): VNode = VNode(kind: VNodeKind.text, text: cstring(s), index: -1) +proc text*(s: cstring): VNode = VNode(kind: VNodeKind.text, text: s, index: -1) iterator items*(n: VNode): VNode = for i in 0.. Date: Tue, 11 Jul 2017 18:59:38 +0200 Subject: [PATCH 094/247] better event handling for components and in the DSL --- src/karax.nim | 1 + src/karaxdsl.nim | 38 ++++++++++++- src/vdom.nim | 19 ++++++- tests/components.nim | 128 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 tests/components.nim diff --git a/src/karax.nim b/src/karax.nim index c96901b..f284d31 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -61,6 +61,7 @@ template setNativeValue(ev, val) = cast[Element](ev.target).value = val template keyeventBody() = let v = nativeValue(ev) n.value = v + assert action != nil action(ev, n) if n.value != v: setNativeValue(ev, n.value) diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index c17d725..cfbe391 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -59,6 +59,19 @@ proc tcall2(n, tmpContext: NimNode): NimNode = result.add n[0] for i in 1 ..< n.len: result.add tcall2(n[i], tmpContext) + of nnkProcDef: + let name = getName n[0] + if name.startsWith"on": + # turn it into an anon proc: + let anon = copyNimTree(n) + anon[0] = newEmptyNode() + if tmpContext == nil: + error "no VNode to attach the event handler to" + else: + result = newCall(bindSym"addEventHandler", tmpContext, + newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi")) + else: + result = n of nnkVarSection, nnkLetSection, nnkConstSection: result = n of nnkCallKinds: @@ -87,6 +100,8 @@ proc tcall2(n, tmpContext: NimNode): NimNode = result.add newDotAsgn(tmp, key, x[1]) elif eqIdent(key, "setFocus"): result.add newCall(key, tmp, x[1], ident"kxi") + elif eqIdent(key, "events"): + result.add newCall(bindSym"mergeEvents", tmp, x[1]) else: result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1]) elif ck != ComponentKind.Tag: @@ -100,7 +115,28 @@ proc tcall2(n, tmpContext: NimNode): NimNode = else: result.add newCall(bindSym"add", tmpContext, tmp) elif tmpContext != nil and op notin StmtContext: - result = newCall(bindSym"add", tmpContext, n) + var hasEventHandlers = false + for i in 1.. 0: + persons[index] = v + errmsg = "" + else: + errmsg = "name must not be empty" + button: + proc onclick(ev: Event; n: VNode) = + persons.delete(index) + errmsg = "" + echo persons + text "(x)" + + +proc createDom(): VNode = + result = buildHtml(tdiv): + tdiv: + for index, text in persons.pairs: + renderPerson(text, index) + tdiv: + newTextInput(VStyle(), &persons.len, ""): + proc onkeyuplater(ev: Event; n: VNode) = + let v = n.value + if v.len > 0: + persons.add v + errmsg = "" + else: + errmsg = "name must not be empty" + tdiv: + text errmsg + +setRenderer createDom From b3c83ed11998efb03b2a578ca0832430e7457343 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Tue, 11 Jul 2017 20:41:01 +0200 Subject: [PATCH 095/247] optimized the diff algorithm further --- src/karax.nim | 55 +++++++++++++++++++++++++++++++++++++++++++-------- src/vdom.nim | 2 +- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index f284d31..6c048cc 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -190,13 +190,33 @@ type EqResult = enum componentsIdentical, different, similar, identical, usenewNode + DifferEnum = enum + deKind, deId, deIndex, deText, deComponent, deClass, + deSimilar + +when defined(profileKarax): + var + reasons: array[DifferEnum, int] + + proc echa(a: array[DifferEnum, int]) = + for i in low(DifferEnum)..high(DifferEnum): + echo i, " value: ", a[i] + proc eq(a, b: VNode): EqResult = - if a.kind != b.kind: return different - if a.id != b.id: return different + if a.kind != b.kind: + when defined(profileKarax): inc reasons[deKind] + return different + if a.id != b.id: + when defined(profileKarax): inc reasons[deId] + return different result = identical - if a.index != b.index: return different + if a.index != b.index: + when defined(profileKarax): inc reasons[deIndex] + return different if a.kind == VNodeKind.text: - if a.text != b.text: return different + if a.text != b.text: + when defined(profileKarax): inc reasons[deText] + return similar elif a.kind == VNodeKind.vthunk or a.kind == VNodeKind.dthunk: if a.text != b.text: return different if a.len != b.len: return different @@ -204,11 +224,19 @@ proc eq(a, b: VNode): EqResult = if eq(a[i], b[i]) == different: return different elif b.kind == VNodeKind.component: # different component names mean different components: - if a.text != b.text: return different - if VComponent(a).key != VComponent(b).key: return different + if a.text != b.text: + when defined(profileKarax): inc reasons[deComponent] + return different + if VComponent(a).key != VComponent(b).key: + when defined(profileKarax): inc reasons[deComponent] + return different return componentsIdentical - if a.class != b.class: return different - if not eq(a.style, b.style) or not sameAttrs(a, b): return similar + if a.class != b.class: + when defined(profileKarax): inc reasons[deClass] + return different + if not eq(a.style, b.style) or not sameAttrs(a, b): + when defined(profileKarax): inc reasons[deSimilar] + return similar # Do not test event listeners here! return result @@ -328,6 +356,10 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E if result == similar: updateStyles(newNode, oldNode) updateAttributes(newNode, oldNode) + if oldNode.kind == VNodeKind.text: + oldNode.text = newNode.text + oldNode.dom.nodeValue = newNode.text + if newNode.events.len != 0 or oldNode.events.len != 0: mergeEvents(newNode, oldNode, kxi) if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea: @@ -471,7 +503,14 @@ proc dodraw(kxi: KaraxInstance) = let olddom = document.getElementById(kxi.rootId) discard diff(newtree, kxi.currentTree, nil, olddom, kxi) #kout cstring"patch len ", patches.len + when defined(profileKarax): + echo "<<<<<<<<<<<<<<" + echa reasons applyComponents(kxi) + when defined(profileKarax): + echo "--------------" + echa reasons + echo ">>>>>>>>>>>>>>" applyPatch(kxi) kxi.currentTree = newtree doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) diff --git a/src/vdom.nim b/src/vdom.nim index 4ffb7f4..2e3b7dd 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -217,7 +217,7 @@ proc getAttr*(n: VNode; key: cstring): cstring = if n.attrs[i] == key: return n.attrs[i+1] proc takeOverAttr*(newNode, oldNode: VNode) = - oldNode.attrs = newNode.attrs + shallowCopy oldNode.attrs, newNode.attrs proc len*(x: VNode): int = x.kids.len proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx] From e4d8cb4c206571751c12f59d2ae72e8f0a30f75a Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Tue, 11 Jul 2017 22:26:46 +0200 Subject: [PATCH 096/247] make tests green again --- tests/difftest.nim | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/tests/difftest.nim b/tests/difftest.nim index 3fbb800..cfdd9b4 100644 --- a/tests/difftest.nim +++ b/tests/difftest.nim @@ -52,12 +52,16 @@ proc testInsert() = let a = buildHtml(tdiv): ul: li: text "A" - li: text "C" + li: + button: + text "C" let b = buildHtml(tdiv): ul: li: text "A" li: text "B" - li: text "C" + li: + button: + text "C" doDiff(a, b, "pkInsertBefore li B") proc testInsert2() = @@ -66,7 +70,9 @@ proc testInsert2() = tdiv: ul: li: text "A" - li: text "D" + li: + button: + text "D" let b = buildHtml(tdiv): tdiv: tdiv: @@ -74,7 +80,9 @@ proc testInsert2() = li: text "A" li: text "B" li: text "C" - li: text "D" + li: + button: + text "D" doDiff(a, b, "pkInsertBefore li B", "pkInsertBefore li C") proc testDelete() = @@ -108,7 +116,8 @@ proc testDeleteMiddle() = li: tdiv: text "G" li: - tdiv: text "H" + button: + tdiv: text "H" let b = buildHtml(tdiv): ul: li: @@ -124,7 +133,8 @@ proc testDeleteMiddle() = li: tdiv: text "F" li: - tdiv: text "H" + button: + tdiv: text "H" doDiff(a, b, "pkDetach li div G", "pkRemove nil") proc createEntry(id: cstring): VNode = @@ -146,14 +156,14 @@ proc testWild() = let a = createEntries(entries) entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")] let b = createEntries(entries) - doDiff(a, b, "pkDetach button 7", "pkRemove nil", "pkDetach div 7", "pkRemove nil") + doDiff(a, b, "pkDetach button 7", "pkRemove nil", "pkDetach div 5", "pkRemove nil") proc testWildInsert() = var entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")] let a = createEntries(entries) entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring"7", cstring("5")] let b = createEntries(entries) - doDiff(a, b, "pkInsertBefore button 7", "pkInsertBefore div 7") + doDiff(a, b, "pkInsertBefore button 7", "pkAppend div 5") kxi = KaraxInstance(rootId: cstring"ROOT", renderer: proc (): VNode = discard) From 2ced69038786eb8456825ef46fb362360248c461 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 12 Jul 2017 12:23:24 +0200 Subject: [PATCH 097/247] better event merging for components --- src/karax.nim | 12 ++++++++---- src/vdom.nim | 6 +++--- tests/components.nim | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 6c048cc..d5de6f5 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -190,11 +190,12 @@ type EqResult = enum componentsIdentical, different, similar, identical, usenewNode - DifferEnum = enum - deKind, deId, deIndex, deText, deComponent, deClass, - deSimilar - when defined(profileKarax): + type + DifferEnum = enum + deKind, deId, deIndex, deText, deComponent, deClass, + deSimilar + var reasons: array[DifferEnum, int] @@ -227,6 +228,9 @@ proc eq(a, b: VNode): EqResult = if a.text != b.text: when defined(profileKarax): inc reasons[deComponent] return different + if VComponent(a).key.isNil and VComponent(b).key.isNil: + when defined(profileKarax): inc reasons[deComponent] + return different if VComponent(a).key != VComponent(b).key: when defined(profileKarax): inc reasons[deComponent] return different diff --git a/src/vdom.nim b/src/vdom.nim index 2e3b7dd..f31d6d6 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -160,17 +160,17 @@ proc vthunk*(name: cstring; args: varargs[VNode, vn]): VNode = proc dthunk*(name: cstring; args: varargs[VNode, vn]): VNode = VNode(kind: VNodeKind.dthunk, text: name, index: -1, kids: @args) -proc setEvent(v: VNode; kind: EventKind; handler: EventHandler) = +proc setEventIfNoConflict(v: VNode; kind: EventKind; handler: EventHandler) = assert handler != nil for i in 0.. 0: persons.add v From e94071b077d125534d8e95006c1b7c4e74171b27 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 12 Jul 2017 13:23:58 +0200 Subject: [PATCH 098/247] components work without 'key' attribute set --- src/karax.nim | 6 +++--- tests/components.nim | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index d5de6f5..9d74a9e 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -228,9 +228,9 @@ proc eq(a, b: VNode): EqResult = if a.text != b.text: when defined(profileKarax): inc reasons[deComponent] return different - if VComponent(a).key.isNil and VComponent(b).key.isNil: - when defined(profileKarax): inc reasons[deComponent] - return different + #if VComponent(a).key.isNil and VComponent(b).key.isNil: + # when defined(profileKarax): inc reasons[deComponent] + # return different if VComponent(a).key != VComponent(b).key: when defined(profileKarax): inc reasons[deComponent] return different diff --git a/tests/components.nim b/tests/components.nim index 99e1bed..6024689 100644 --- a/tests/components.nim +++ b/tests/components.nim @@ -54,7 +54,7 @@ proc newTextInput*(style: VStyle = VStyle(); key: cstring; result = newComponent(TextInput, render, changed=changed, updated=update) result.style = style result.value = value - result.key = key + #result.key = key when false: type From f9525dc646224afe8988303e87dc8ffafc5fae94 Mon Sep 17 00:00:00 2001 From: Araq Date: Fri, 14 Jul 2017 16:17:06 +0200 Subject: [PATCH 099/247] added support for i18n (candidate for the stdlib?) --- src/i18n.nim | 152 ++++++++++++ src/languages.nim | 616 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 768 insertions(+) create mode 100644 src/i18n.nim create mode 100644 src/languages.nim diff --git a/src/i18n.nim b/src/i18n.nim new file mode 100644 index 0000000..42274ff --- /dev/null +++ b/src/i18n.nim @@ -0,0 +1,152 @@ +## Karax -- Single page applications for Nim. + +## i18n support for Karax applications. We distinguish between: +## +## - translate: Lookup text translations +## - localize: Localize Date and Time objects to local formats +## +## Localization has not yet been implemented. + +import languages, jdict, jstrutils + +var currentLanguage = Language.enUS + +proc getLanguage(): cstring = + {.emit: "`result` = (navigator.languages && navigator.languages.length) ? navigator.languages[0] : navigator.language;".} + +proc detectLanguage*(): Language = + let x = getLanguage() + for i in low(Language)..high(Language): + if languageToCode[i] == x: return i + return Language.enUS + +proc setCurrentLanguage*(newLanguage = detectLanguage()) = + currentLanguage = newLanguage + +proc getCurrentLanguage*(): Language = currentLanguage + +type Translation* = JDict[cstring, cstring] + +var + translations: array[Language, Translation] + +proc registerTranslation*(lang: Language; t: Translation) = translations[lang] = t + +proc addTranslation*(lang: Language; key, val: cstring) = + if translations[lang].isNil: translations[lang] = newJDict[cstring, cstring]() + translations[lang][key] = val + +proc translate(x: cstring): cstring = + let y = translations[currentLanguage] + if y != nil and y.contains(x): + result = y[x] + else: + result = x + +type TranslatedString* = distinct cstring + +template i18n*(x: string): TranslatedString = TranslatedString(translate(cstring x)) + +proc raiseInvalidFormat(errmsg: string) = + raise newException(ValueError, errmsg) + +discard """ + $[1:item|items]1$ selected. +""" + +proc `{}`(x: cstring; i: int): char {.importcpp: "#.charCodeAt(#)".} +proc sadd(s: JSeq[char]; c: char) {.importcpp: "#.push(String.fromCharCode(#))".} + +proc parseChoice(f: cstring; i, choice: int, + r: JSeq[char]) = + var i = i + while i < f.len: + var n = 0 + let oldI = i + var toAdd = false + while i < f.len and f{i} >= '0' and f{i} <= '9': + n = n * 10 + ord(f{i}) - ord('0') + inc i + if oldI != i: + if f{i} == ':': + inc i + else: + raiseInvalidFormat"':' after number expected" + toAdd = choice == n + else: + # an else section does not start with a number: + toAdd = true + while i < f.len and f{i} != ']' and f{i} != '|': + if toAdd: r.sadd f{i} + inc i + if toAdd: break + inc i + +proc join(x: JSeq[char]): cstring {.importcpp: "#.join(\"\")".} +proc add(x: JSeq[char]; y: cstring) = + for i in 0..= '0' and f{i} <= '9': + j = j * 10 + ord(f{i}) - ord('0') + inc i + let idx = if not negative: j-1 else: args.len-j + r.add args[idx] + of '$': + inc(i) + r.add '$' + of '[': + let start = i+1 + while i < f.len and f{i} != ']': inc i + inc i + if i >= f.len: raiseInvalidFormat"']' expected" + case f{i} + of '#': + parseChoice(f, start, parseInt args[num], r) + inc i + inc num + of '1'..'9', '-': + var j = 0 + var negative = f{i} == '-' + if negative: inc i + while f{i} >= '0' and f{i} <= '9': + j = j * 10 + ord(f{i}) - ord('0') + inc i + let idx = if not negative: j-1 else: args.len-j + parseChoice(f, start, parseInt args[idx], r) + else: raiseInvalidFormat"argument index expected after ']'" + else: + raiseInvalidFormat("'#', '$', or number expected") + if i < f.len and f{i} == '$': inc i + else: + r.sadd f{i} + inc i + result = join(r) + + +when isMainModule: + addTranslation(Language.deDE, "$#$ -> $#$", "$2 macht $1") + setCurrentLanguage(Language.deDE) + + echo(i18n"$#$ -> $#$" % [cstring"1", "2"]) + echo(i18n"$[1:item |items ]1$ -> $1" % [cstring"1", "2"]) + echo(i18n"$[1:item |items ]1 -> $1" % [cstring"0", "2"]) + echo(i18n"$[1:item |items ]1 -> $1" % [cstring"2", "2"]) + echo(i18n"$[1:item |items ]1$ -> $1" % [cstring"3", "2"]) + echo(i18n"$1 $[1:item |4:huha |items ]1$ -> $1" % [cstring"4", "2"]) diff --git a/src/languages.nim b/src/languages.nim new file mode 100644 index 0000000..6b6b027 --- /dev/null +++ b/src/languages.nim @@ -0,0 +1,616 @@ +## Karax -- Single page applications for Nim. + +type + Language* {.pure.} = enum ## A pure enum that contains most (all?) + ## known human languages in current use. + afZA ## "South Africa - Afrikaans" + amET ## "Ethiopia - Amharic" + arAE ## "U.A.E. - Arabic" + arBH ## "Bahrain - Arabic" + arDZ ## "Algeria - Arabic" + arEG ## "Egypt - Arabic" + arIQ ## "Iraq - Arabic" + arJO ## "Jordan - Arabic" + arKW ## "Kuwait - Arabic" + arLB ## "Lebanon - Arabic" + arLY ## "Libya - Arabic" + arMA ## "Morocco - Arabic" + arOM ## "Oman - Arabic" + arQA ## "Qatar - Arabic" + arSA ## "Saudi Arabia - Arabic" + arSY ## "Syria - Arabic" + arTN ## "Tunisia - Arabic" + arYE ## "Yemen - Arabic" + arnCL ## "Chile - Mapudungun" + asIN ## "India - Assamese" + azAZ ## "Azerbaijan - Azeri (Cyrillic)" + baRU ## "Russia - Bashkir" + beBY ## "Belarus - Belarusian" + bgBG ## "Bulgaria - Bulgarian" + bnBD ## "Bangladesh - Bengali" + bnIN ## "India - Bengali" + boCN ## "People's Republic of China - Tibetan" + brFR ## "France - Breton" + bsBA ## "Bosnia and Herzegovina - Bosnian (Cyrillic)" + caES ## "Spain - Catalan" + coFR ## "France - Corsican" + csCZ ## "Czech Republic - Czech" + cyGB ## "United Kingdom - Welsh" + daDK ## "Denmark - Danish" + deAT ## "Austria - German" + deCH ## "Switzerland - German" + deDE ## "Germany - German" + deLI ## "Liechtenstein - German" + deLU ## "Luxembourg - German" + dsbDE ## "Germany - Lower Sorbian" + dvMV ## "Maldives - Divehi" + elGR ## "Greece - Greek" + en029 ## "Caribbean - English" + enAU ## "Australia - English" + enBZ ## "Belize - English" + enCA ## "Canada - English" + enGB ## "United Kingdom - English" + enIE ## "Ireland - English" + enIN ## "India - English" + enJM ## "Jamaica - English" + enMY ## "Malaysia - English" + enNZ ## "New Zealand - English" + enPH ## "Republic of the Philippines - English" + enSG ## "Singapore - English" + enTT ## "Trinidad and Tobago - English" + enUS ## "United States - English" + enZA ## "South Africa - English" + enZW ## "Zimbabwe - English" + esAR ## "Argentina - Spanish" + esBO ## "Bolivia - Spanish" + esCL ## "Chile - Spanish" + esCO ## "Colombia - Spanish" + esCR ## "Costa Rica - Spanish" + esDO ## "Dominican Republic - Spanish" + esEC ## "Ecuador - Spanish" + esES ## "Spain - Spanish" + esGT ## "Guatemala - Spanish" + esHN ## "Honduras - Spanish" + esMX ## "Mexico - Spanish" + esNI ## "Nicaragua - Spanish" + esPA ## "Panama - Spanish" + esPE ## "Peru - Spanish" + esPR ## "Puerto Rico - Spanish" + esPY ## "Paraguay - Spanish" + esSV ## "El Salvador - Spanish" + esUS ## "United States - Spanish" + esUY ## "Uruguay - Spanish" + esVE ## "Bolivarian Republic of Venezuela - Spanish" + etEE ## "Estonia - Estonian" + euES ## "Spain - Basque" + faIR ## "Iran - Persian" + fiFI ## "Finland - Finnish" + filPH ## "Philippines - Filipino" + foFO ## "Faroe Islands - Faroese" + frBE ## "Belgium - French" + frCA ## "Canada - French" + frCH ## "Switzerland - French" + frFR ## "France - French" + frLU ## "Luxembourg - French" + frMC ## "Principality of Monaco - French" + fyNL ## "Netherlands - Frisian" + gaIE ## "Ireland - Irish" + gdGB ## "United Kingdom - Scottish Gaelic" + glES ## "Spain - Galician" + gswFR ## "France - Alsatian" + guIN ## "India - Gujarati" + haNG ## "Nigeria - Hausa (Latin)" + heIL ## "Israel - Hebrew" + hiIN ## "India - Hindi" + hrBA ## "Bosnia and Herzegovina - Croatian" + hrHR ## "Croatia - Croatian" + hsbDE ## "Germany - Upper Sorbian" + huHU ## "Hungary - Hungarian" + hyAM ## "Armenia - Armenian" + idID ## "Indonesia - Indonesian" + igNG ## "Nigeria - Igbo" + iiCN ## "People's Republic of China - Yi" + isIS ## "Iceland - Icelandic" + itCH ## "Switzerland - Italian" + itIT ## "Italy - Italian" + iuCA ## "Canada - Inuktitut (Latin)" + jaJP ## "Japan - Japanese" + kaGE ## "Georgia - Georgian" + kkKZ ## "Kazakhstan - Kazakh" + klGL ## "Greenland - Greenlandic" + kmKH ## "Cambodia - Khmer" + knIN ## "India - Kannada" + koKR ## "Korea - Korean" + kokIN ## "India - Konkani" + kyKG ## "Kyrgyzstan - Kyrgyz" + lbLU ## "Luxembourg - Luxembourgish" + loLA ## "Lao P.D.R. - Lao" + ltLT ## "Lithuania - Lithuanian" + lvLV ## "Latvia - Latvian" + miNZ ## "New Zealand - Maori" + mkMK ## "Macedonia (FYROM) - Macedonian (FYROM)" + mlIN ## "India - Malayalam" + mnCN ## "People's Republic of China - Mongolian (Traditional Mongolian)" + mnMN ## "Mongolia - Mongolian (Cyrillic)" + mohCA ## "Canada - Mohawk" + mrIN ## "India - Marathi" + msBN ## "Brunei Darussalam - Malay" + msMY ## "Malaysia - Malay" + mtMT ## "Malta - Maltese" + nbNO ## "Norway - Norwegian (Bokmal)" + neNP ## "Nepal - Nepali" + nlBE ## "Belgium - Dutch" + nlNL ## "Netherlands - Dutch" + nnNO ## "Norway - Norwegian (Nynorsk)" + nsoZA ## "South Africa - Sesotho sa Leboa" + ocFR ## "France - Occitan" + orIN ## "India - Oriya" + paIN ## "India - Punjabi" + plPL ## "Poland - Polish" + prsAF ## "Afghanistan - Dari" + psAF ## "Afghanistan - Pashto" + ptBR ## "Brazil - Portuguese" + ptPT ## "Portugal - Portuguese" + qutGT ## "Guatemala - K'iche" + quzBO ## "Bolivia - Quechua" + quzEC ## "Ecuador - Quechua" + quzPE ## "Peru - Quechua" + rmCH ## "Switzerland - Romansh" + roRO ## "Romania - Romanian" + ruRU ## "Russia - Russian" + rwRW ## "Rwanda - Kinyarwanda" + saIN ## "India - Sanskrit" + sahRU ## "Russia - Yakut" + seFI ## "Finland - Sami (Northern)" + seNO ## "Norway - Sami (Northern)" + seSE ## "Sweden - Sami (Northern)" + siLK ## "Sri Lanka - Sinhala" + skSK ## "Slovakia - Slovak" + slSI ## "Slovenia - Slovenian" + smaNO ## "Norway - Sami (Southern)" + smaSE ## "Sweden - Sami (Southern)" + smjNO ## "Norway - Sami (Lule)" + smjSE ## "Sweden - Sami (Lule)" + smnFI ## "Finland - Sami (Inari)" + smsFI ## "Finland - Sami (Skolt)" + sqAL ## "Albania - Albanian" + srBA ## "Bosnia and Herzegovina - Serbian (Cyrillic)" + srCS ## "Serbia and Montenegro (Former) - Serbian (Cyrillic)" + srME ## "Montenegro - Serbian (Cyrillic)" + srRS ## "Serbia - Serbian (Cyrillic)" + svFI ## "Finland - Swedish" + svSE ## "Sweden - Swedish" + swKE ## "Kenya - Kiswahili" + syrSY ## "Syria - Syriac" + taIN ## "India - Tamil" + teIN ## "India - Telugu" + tgTJ ## "Tajikistan - Tajik (Cyrillic)" + thTH ## "Thailand - Thai" + tkTM ## "Turkmenistan - Turkmen" + tnZA ## "South Africa - Setswana" + trTR ## "Turkey - Turkish" + ttRU ## "Russia - Tatar" + tzmDZ ## "Algeria - Tamazight (Latin)" + ugCN ## "People's Republic of China - Uyghur" + ukUA ## "Ukraine - Ukrainian" + urPK ## "Islamic Republic of Pakistan - Urdu" + uzUZ ## "Uzbekistan - Uzbek (Cyrillic)" + viVN ## "Vietnam - Vietnamese" + woSN ## "Senegal - Wolof" + xhZA ## "South Africa - isiXhosa" + yoNG ## "Nigeria - Yoruba" + zhCN ## "People's Republic of China - Chinese (Simplified) Legacy" + zhHK ## "Hong Kong S.A.R. - Chinese (Traditional) Legacy" + zhMO ## "Macao S.A.R. - Chinese (Traditional) Legacy" + zhSG ## "Singapore - Chinese (Simplified) Legacy" + zhTW ## "Taiwan - Chinese (Traditional) Legacy" + zuZA ## "South Africa - isiZulu" + +const + languageToName*: array[Language, cstring] = [ + Language.afZA: cstring"South Africa - Afrikaans", + Language.amET: cstring"Ethiopia - Amharic", + Language.arAE: cstring"U.A.E. - Arabic", + Language.arBH: cstring"Bahrain - Arabic", + Language.arDZ: cstring"Algeria - Arabic", + Language.arEG: cstring"Egypt - Arabic", + Language.arIQ: cstring"Iraq - Arabic", + Language.arJO: cstring"Jordan - Arabic", + Language.arKW: cstring"Kuwait - Arabic", + Language.arLB: cstring"Lebanon - Arabic", + Language.arLY: cstring"Libya - Arabic", + Language.arMA: cstring"Morocco - Arabic", + Language.arOM: cstring"Oman - Arabic", + Language.arQA: cstring"Qatar - Arabic", + Language.arSA: cstring"Saudi Arabia - Arabic", + Language.arSY: cstring"Syria - Arabic", + Language.arTN: cstring"Tunisia - Arabic", + Language.arYE: cstring"Yemen - Arabic", + Language.arnCL: cstring"Chile - Mapudungun", + Language.asIN: cstring"India - Assamese", + Language.azAZ: cstring"Azerbaijan - Azeri (Cyrillic)", + Language.baRU: cstring"Russia - Bashkir", + Language.beBY: cstring"Belarus - Belarusian", + Language.bgBG: cstring"Bulgaria - Bulgarian", + Language.bnBD: cstring"Bangladesh - Bengali", + Language.bnIN: cstring"India - Bengali", + Language.boCN: cstring"People's Republic of China - Tibetan", + Language.brFR: cstring"France - Breton", + Language.bsBA: cstring"Bosnia and Herzegovina - Bosnian (Cyrillic)", + Language.caES: cstring"Spain - Catalan", + Language.coFR: cstring"France - Corsican", + Language.csCZ: cstring"Czech Republic - Czech", + Language.cyGB: cstring"United Kingdom - Welsh", + Language.daDK: cstring"Denmark - Danish", + Language.deAT: cstring"Austria - German", + Language.deCH: cstring"Switzerland - German", + Language.deDE: cstring"Germany - German", + Language.deLI: cstring"Liechtenstein - German", + Language.deLU: cstring"Luxembourg - German", + Language.dsbDE: cstring"Germany - Lower Sorbian", + Language.dvMV: cstring"Maldives - Divehi", + Language.elGR: cstring"Greece - Greek", + Language.en029: cstring"Caribbean - English", + Language.enAU: cstring"Australia - English", + Language.enBZ: cstring"Belize - English", + Language.enCA: cstring"Canada - English", + Language.enGB: cstring"United Kingdom - English", + Language.enIE: cstring"Ireland - English", + Language.enIN: cstring"India - English", + Language.enJM: cstring"Jamaica - English", + Language.enMY: cstring"Malaysia - English", + Language.enNZ: cstring"New Zealand - English", + Language.enPH: cstring"Republic of the Philippines - English", + Language.enSG: cstring"Singapore - English", + Language.enTT: cstring"Trinidad and Tobago - English", + Language.enUS: cstring"United States - English", + Language.enZA: cstring"South Africa - English", + Language.enZW: cstring"Zimbabwe - English", + Language.esAR: cstring"Argentina - Spanish", + Language.esBO: cstring"Bolivia - Spanish", + Language.esCL: cstring"Chile - Spanish", + Language.esCO: cstring"Colombia - Spanish", + Language.esCR: cstring"Costa Rica - Spanish", + Language.esDO: cstring"Dominican Republic - Spanish", + Language.esEC: cstring"Ecuador - Spanish", + Language.esES: cstring"Spain - Spanish", + Language.esGT: cstring"Guatemala - Spanish", + Language.esHN: cstring"Honduras - Spanish", + Language.esMX: cstring"Mexico - Spanish", + Language.esNI: cstring"Nicaragua - Spanish", + Language.esPA: cstring"Panama - Spanish", + Language.esPE: cstring"Peru - Spanish", + Language.esPR: cstring"Puerto Rico - Spanish", + Language.esPY: cstring"Paraguay - Spanish", + Language.esSV: cstring"El Salvador - Spanish", + Language.esUS: cstring"United States - Spanish", + Language.esUY: cstring"Uruguay - Spanish", + Language.esVE: cstring"Bolivarian Republic of Venezuela - Spanish", + Language.etEE: cstring"Estonia - Estonian", + Language.euES: cstring"Spain - Basque", + Language.faIR: cstring"Iran - Persian", + Language.fiFI: cstring"Finland - Finnish", + Language.filPH: cstring"Philippines - Filipino", + Language.foFO: cstring"Faroe Islands - Faroese", + Language.frBE: cstring"Belgium - French", + Language.frCA: cstring"Canada - French", + Language.frCH: cstring"Switzerland - French", + Language.frFR: cstring"France - French", + Language.frLU: cstring"Luxembourg - French", + Language.frMC: cstring"Principality of Monaco - French", + Language.fyNL: cstring"Netherlands - Frisian", + Language.gaIE: cstring"Ireland - Irish", + Language.gdGB: cstring"United Kingdom - Scottish Gaelic", + Language.glES: cstring"Spain - Galician", + Language.gswFR: cstring"France - Alsatian", + Language.guIN: cstring"India - Gujarati", + Language.haNG: cstring"Nigeria - Hausa (Latin)", + Language.heIL: cstring"Israel - Hebrew", + Language.hiIN: cstring"India - Hindi", + Language.hrBA: cstring"Bosnia and Herzegovina - Croatian", + Language.hrHR: cstring"Croatia - Croatian", + Language.hsbDE: cstring"Germany - Upper Sorbian", + Language.huHU: cstring"Hungary - Hungarian", + Language.hyAM: cstring"Armenia - Armenian", + Language.idID: cstring"Indonesia - Indonesian", + Language.igNG: cstring"Nigeria - Igbo", + Language.iiCN: cstring"People's Republic of China - Yi", + Language.isIS: cstring"Iceland - Icelandic", + Language.itCH: cstring"Switzerland - Italian", + Language.itIT: cstring"Italy - Italian", + Language.iuCA: cstring"Canada - Inuktitut (Latin)", + Language.jaJP: cstring"Japan - Japanese", + Language.kaGE: cstring"Georgia - Georgian", + Language.kkKZ: cstring"Kazakhstan - Kazakh", + Language.klGL: cstring"Greenland - Greenlandic", + Language.kmKH: cstring"Cambodia - Khmer", + Language.knIN: cstring"India - Kannada", + Language.koKR: cstring"Korea - Korean", + Language.kokIN: cstring"India - Konkani", + Language.kyKG: cstring"Kyrgyzstan - Kyrgyz", + Language.lbLU: cstring"Luxembourg - Luxembourgish", + Language.loLA: cstring"Lao P.D.R. - Lao", + Language.ltLT: cstring"Lithuania - Lithuanian", + Language.lvLV: cstring"Latvia - Latvian", + Language.miNZ: cstring"New Zealand - Maori", + Language.mkMK: cstring"Macedonia (FYROM) - Macedonian (FYROM)", + Language.mlIN: cstring"India - Malayalam", + Language.mnCN: cstring"People's Republic of China - Mongolian (Traditional Mongolian)", + Language.mnMN: cstring"Mongolia - Mongolian (Cyrillic)", + Language.mohCA: cstring"Canada - Mohawk", + Language.mrIN: cstring"India - Marathi", + Language.msBN: cstring"Brunei Darussalam - Malay", + Language.msMY: cstring"Malaysia - Malay", + Language.mtMT: cstring"Malta - Maltese", + Language.nbNO: cstring"Norway - Norwegian (Bokmal)", + Language.neNP: cstring"Nepal - Nepali", + Language.nlBE: cstring"Belgium - Dutch", + Language.nlNL: cstring"Netherlands - Dutch", + Language.nnNO: cstring"Norway - Norwegian (Nynorsk)", + Language.nsoZA: cstring"South Africa - Sesotho sa Leboa", + Language.ocFR: cstring"France - Occitan", + Language.orIN: cstring"India - Oriya", + Language.paIN: cstring"India - Punjabi", + Language.plPL: cstring"Poland - Polish", + Language.prsAF: cstring"Afghanistan - Dari", + Language.psAF: cstring"Afghanistan - Pashto", + Language.ptBR: cstring"Brazil - Portuguese", + Language.ptPT: cstring"Portugal - Portuguese", + Language.qutGT: cstring"Guatemala - K'iche", + Language.quzBO: cstring"Bolivia - Quechua", + Language.quzEC: cstring"Ecuador - Quechua", + Language.quzPE: cstring"Peru - Quechua", + Language.rmCH: cstring"Switzerland - Romansh", + Language.roRO: cstring"Romania - Romanian", + Language.ruRU: cstring"Russia - Russian", + Language.rwRW: cstring"Rwanda - Kinyarwanda", + Language.saIN: cstring"India - Sanskrit", + Language.sahRU: cstring"Russia - Yakut", + Language.seFI: cstring"Finland - Sami (Northern)", + Language.seNO: cstring"Norway - Sami (Northern)", + Language.seSE: cstring"Sweden - Sami (Northern)", + Language.siLK: cstring"Sri Lanka - Sinhala", + Language.skSK: cstring"Slovakia - Slovak", + Language.slSI: cstring"Slovenia - Slovenian", + Language.smaNO: cstring"Norway - Sami (Southern)", + Language.smaSE: cstring"Sweden - Sami (Southern)", + Language.smjNO: cstring"Norway - Sami (Lule)", + Language.smjSE: cstring"Sweden - Sami (Lule)", + Language.smnFI: cstring"Finland - Sami (Inari)", + Language.smsFI: cstring"Finland - Sami (Skolt)", + Language.sqAL: cstring"Albania - Albanian", + Language.srBA: cstring"Bosnia and Herzegovina - Serbian (Cyrillic)", + Language.srCS: cstring"Serbia and Montenegro (Former) - Serbian (Cyrillic)", + Language.srME: cstring"Montenegro - Serbian (Cyrillic)", + Language.srRS: cstring"Serbia - Serbian (Cyrillic)", + Language.svFI: cstring"Finland - Swedish", + Language.svSE: cstring"Sweden - Swedish", + Language.swKE: cstring"Kenya - Kiswahili", + Language.syrSY: cstring"Syria - Syriac", + Language.taIN: cstring"India - Tamil", + Language.teIN: cstring"India - Telugu", + Language.tgTJ: cstring"Tajikistan - Tajik (Cyrillic)", + Language.thTH: cstring"Thailand - Thai", + Language.tkTM: cstring"Turkmenistan - Turkmen", + Language.tnZA: cstring"South Africa - Setswana", + Language.trTR: cstring"Turkey - Turkish", + Language.ttRU: cstring"Russia - Tatar", + Language.tzmDZ: cstring"Algeria - Tamazight (Latin)", + Language.ugCN: cstring"People's Republic of China - Uyghur", + Language.ukUA: cstring"Ukraine - Ukrainian", + Language.urPK: cstring"Islamic Republic of Pakistan - Urdu", + Language.uzUZ: cstring"Uzbekistan - Uzbek (Cyrillic)", + Language.viVN: cstring"Vietnam - Vietnamese", + Language.woSN: cstring"Senegal - Wolof", + Language.xhZA: cstring"South Africa - isiXhosa", + Language.yoNG: cstring"Nigeria - Yoruba", + Language.zhCN: cstring"People's Republic of China - Chinese (Simplified) Legacy", + Language.zhHK: cstring"Hong Kong S.A.R. - Chinese (Traditional) Legacy", + Language.zhMO: cstring"Macao S.A.R. - Chinese (Traditional) Legacy", + Language.zhSG: cstring"Singapore - Chinese (Simplified) Legacy", + Language.zhTW: cstring"Taiwan - Chinese (Traditional) Legacy", + Language.zuZA: cstring"South Africa - isiZulu"] + + languageToCode*: array[Language, cstring] = [ + Language.afZA: cstring"af-ZA", + Language.amET: cstring"am-ET", + Language.arAE: cstring"ar-AE", + Language.arBH: cstring"ar-BH", + Language.arDZ: cstring"ar-DZ", + Language.arEG: cstring"ar-EG", + Language.arIQ: cstring"ar-IQ", + Language.arJO: cstring"ar-JO", + Language.arKW: cstring"ar-KW", + Language.arLB: cstring"ar-LB", + Language.arLY: cstring"ar-LY", + Language.arMA: cstring"ar-MA", + Language.arOM: cstring"ar-OM", + Language.arQA: cstring"ar-QA", + Language.arSA: cstring"ar-SA", + Language.arSY: cstring"ar-SY", + Language.arTN: cstring"ar-TN", + Language.arYE: cstring"ar-YE", + Language.arnCL: cstring"arn-CL", + Language.asIN: cstring"as-IN", + Language.azAZ: cstring"az-AZ", + Language.baRU: cstring"ba-RU", + Language.beBY: cstring"be-BY", + Language.bgBG: cstring"bg-BG", + Language.bnBD: cstring"bn-BD", + Language.bnIN: cstring"bn-IN", + Language.boCN: cstring"bo-CN", + Language.brFR: cstring"br-FR", + Language.bsBA: cstring"bs-BA", + Language.caES: cstring"ca-ES", + Language.coFR: cstring"co-FR", + Language.csCZ: cstring"cs-CZ", + Language.cyGB: cstring"cy-GB", + Language.daDK: cstring"da-DK", + Language.deAT: cstring"de-AT", + Language.deCH: cstring"de-CH", + Language.deDE: cstring"de-DE", + Language.deLI: cstring"de-LI", + Language.deLU: cstring"de-LU", + Language.dsbDE: cstring"dsb-DE", + Language.dvMV: cstring"dv-MV", + Language.elGR: cstring"el-GR", + Language.en029: cstring"en-029", + Language.enAU: cstring"en-AU", + Language.enBZ: cstring"en-BZ", + Language.enCA: cstring"en-CA", + Language.enGB: cstring"en-GB", + Language.enIE: cstring"en-IE", + Language.enIN: cstring"en-IN", + Language.enJM: cstring"en-JM", + Language.enMY: cstring"en-MY", + Language.enNZ: cstring"en-NZ", + Language.enPH: cstring"en-PH", + Language.enSG: cstring"en-SG", + Language.enTT: cstring"en-TT", + Language.enUS: cstring"en-US", + Language.enZA: cstring"en-ZA", + Language.enZW: cstring"en-ZW", + Language.esAR: cstring"es-AR", + Language.esBO: cstring"es-BO", + Language.esCL: cstring"es-CL", + Language.esCO: cstring"es-CO", + Language.esCR: cstring"es-CR", + Language.esDO: cstring"es-DO", + Language.esEC: cstring"es-EC", + Language.esES: cstring"es-ES", + Language.esGT: cstring"es-GT", + Language.esHN: cstring"es-HN", + Language.esMX: cstring"es-MX", + Language.esNI: cstring"es-NI", + Language.esPA: cstring"es-PA", + Language.esPE: cstring"es-PE", + Language.esPR: cstring"es-PR", + Language.esPY: cstring"es-PY", + Language.esSV: cstring"es-SV", + Language.esUS: cstring"es-US", + Language.esUY: cstring"es-UY", + Language.esVE: cstring"es-VE", + Language.etEE: cstring"et-EE", + Language.euES: cstring"eu-ES", + Language.faIR: cstring"fa-IR", + Language.fiFI: cstring"fi-FI", + Language.filPH: cstring"fil-PH", + Language.foFO: cstring"fo-FO", + Language.frBE: cstring"fr-BE", + Language.frCA: cstring"fr-CA", + Language.frCH: cstring"fr-CH", + Language.frFR: cstring"fr-FR", + Language.frLU: cstring"fr-LU", + Language.frMC: cstring"fr-MC", + Language.fyNL: cstring"fy-NL", + Language.gaIE: cstring"ga-IE", + Language.gdGB: cstring"gd-GB", + Language.glES: cstring"gl-ES", + Language.gswFR: cstring"gsw-FR", + Language.guIN: cstring"gu-IN", + Language.haNG: cstring"ha-NG", + Language.heIL: cstring"he-IL", + Language.hiIN: cstring"hi-IN", + Language.hrBA: cstring"hr-BA", + Language.hrHR: cstring"hr-HR", + Language.hsbDE: cstring"hsb-DE", + Language.huHU: cstring"hu-HU", + Language.hyAM: cstring"hy-AM", + Language.idID: cstring"id-ID", + Language.igNG: cstring"ig-NG", + Language.iiCN: cstring"ii-CN", + Language.isIS: cstring"is-IS", + Language.itCH: cstring"it-CH", + Language.itIT: cstring"it-IT", + Language.iuCA: cstring"iu-CA", + Language.jaJP: cstring"ja-JP", + Language.kaGE: cstring"ka-GE", + Language.kkKZ: cstring"kk-KZ", + Language.klGL: cstring"kl-GL", + Language.kmKH: cstring"km-KH", + Language.knIN: cstring"kn-IN", + Language.koKR: cstring"ko-KR", + Language.kokIN: cstring"kok-IN", + Language.kyKG: cstring"ky-KG", + Language.lbLU: cstring"lb-LU", + Language.loLA: cstring"lo-LA", + Language.ltLT: cstring"lt-LT", + Language.lvLV: cstring"lv-LV", + Language.miNZ: cstring"mi-NZ", + Language.mkMK: cstring"mk-MK", + Language.mlIN: cstring"ml-IN", + Language.mnCN: cstring"mn-CN", + Language.mnMN: cstring"mn-MN", + Language.mohCA: cstring"moh-CA", + Language.mrIN: cstring"mr-IN", + Language.msBN: cstring"ms-BN", + Language.msMY: cstring"ms-MY", + Language.mtMT: cstring"mt-MT", + Language.nbNO: cstring"nb-NO", + Language.neNP: cstring"ne-NP", + Language.nlBE: cstring"nl-BE", + Language.nlNL: cstring"nl-NL", + Language.nnNO: cstring"nn-NO", + Language.nsoZA: cstring"nso-ZA", + Language.ocFR: cstring"oc-FR", + Language.orIN: cstring"or-IN", + Language.paIN: cstring"pa-IN", + Language.plPL: cstring"pl-PL", + Language.prsAF: cstring"prs-AF", + Language.psAF: cstring"ps-AF", + Language.ptBR: cstring"pt-BR", + Language.ptPT: cstring"pt-PT", + Language.qutGT: cstring"qut-GT", + Language.quzBO: cstring"quz-BO", + Language.quzEC: cstring"quz-EC", + Language.quzPE: cstring"quz-PE", + Language.rmCH: cstring"rm-CH", + Language.roRO: cstring"ro-RO", + Language.ruRU: cstring"ru-RU", + Language.rwRW: cstring"rw-RW", + Language.saIN: cstring"sa-IN", + Language.sahRU: cstring"sah-RU", + Language.seFI: cstring"se-FI", + Language.seNO: cstring"se-NO", + Language.seSE: cstring"se-SE", + Language.siLK: cstring"si-LK", + Language.skSK: cstring"sk-SK", + Language.slSI: cstring"sl-SI", + Language.smaNO: cstring"sma-NO", + Language.smaSE: cstring"sma-SE", + Language.smjNO: cstring"smj-NO", + Language.smjSE: cstring"smj-SE", + Language.smnFI: cstring"smn-FI", + Language.smsFI: cstring"sms-FI", + Language.sqAL: cstring"sq-AL", + Language.srBA: cstring"sr-BA", + Language.srCS: cstring"sr-CS", + Language.srME: cstring"sr-ME", + Language.srRS: cstring"sr-RS", + Language.svFI: cstring"sv-FI", + Language.svSE: cstring"sv-SE", + Language.swKE: cstring"sw-KE", + Language.syrSY: cstring"syr-SY", + Language.taIN: cstring"ta-IN", + Language.teIN: cstring"te-IN", + Language.tgTJ: cstring"tg-TJ", + Language.thTH: cstring"th-TH", + Language.tkTM: cstring"tk-TM", + Language.tnZA: cstring"tn-ZA", + Language.trTR: cstring"tr-TR", + Language.ttRU: cstring"tt-RU", + Language.tzmDZ: cstring"tzm-DZ", + Language.ugCN: cstring"ug-CN", + Language.ukUA: cstring"uk-UA", + Language.urPK: cstring"ur-PK", + Language.uzUZ: cstring"uz-UZ", + Language.viVN: cstring"vi-VN", + Language.woSN: cstring"wo-SN", + Language.xhZA: cstring"xh-ZA", + Language.yoNG: cstring"yo-NG", + Language.zhCN: cstring"zh-CN", + Language.zhHK: cstring"zh-HK", + Language.zhMO: cstring"zh-MO", + Language.zhSG: cstring"zh-SG", + Language.zhTW: cstring"zh-TW", + Language.zuZA: cstring"zu-ZA"] From bb846644b5a6b6f7148712fc67c5c33cbc062f44 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Tue, 8 Aug 2017 13:29:29 +0200 Subject: [PATCH 100/247] added support for a --define:karaxDebug switch --- src/karax.nim | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/karax.nim b/src/karax.nim index 9d74a9e..5cee5cf 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -463,24 +463,38 @@ proc applyComponents(kxi: KaraxInstance) = while i < kxi.components.len: let x = kxi.components[i].oldNode let newNode = kxi.components[i].newNode + when defined(karaxDebug): + echo "Processing component ", newNode.text, " changed impl set ", x.changedImpl != nil if x.changedImpl != nil and x.changedImpl(x, newNode): + when defined(karaxDebug): + echo "Component ", newNode.text, " did change" let current = kxi.components[i].current let parent = kxi.components[i].parent x.updatedImpl(x, newNode) let oldExpanded = x.expanded x.expanded = x.renderImpl(x) + when defined(karaxDebug): + echo "Component ", newNode.text, " re-rendered" x.renderedVersion = x.version if oldExpanded == nil: detach(x) kxi.addPatch(pkReplace, parent, current, x.expanded) + when defined(karaxDebug): + echo "Component ", newNode.text, ": old expansion didn't exist" else: 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)" discard "diff created a patchset for us, so this is fine" elif res != different: + when defined(karaxDebug): + echo "Component ", newNode.text, ": re-render triggered no DOM change whatsoever" x.expanded = oldExpanded assert oldExpanded.dom != nil, "old expanded.dom is nil" else: + when defined(karaxDebug): + echo "Component ", newNode.text, ": re-render triggered a DOM change (case B)" assert x.expanded.dom != nil, "expanded.dom is nil" inc i setLen(kxi.components, 0) From 7c08c6bb76498f05b512ef09b4129a75496ec620 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 16 Aug 2017 19:42:16 +0200 Subject: [PATCH 101/247] first version; wip --- experiments/nextgen.nim | 181 +++++++++++++++++++++++++++++++++++++ experiments/scrollapp.nims | 1 - src/karax.nim | 32 ++++++- src/vdom.nim | 3 +- 4 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 experiments/nextgen.nim delete mode 100644 experiments/scrollapp.nims diff --git a/experiments/nextgen.nim b/experiments/nextgen.nim new file mode 100644 index 0000000..eb94658 --- /dev/null +++ b/experiments/nextgen.nim @@ -0,0 +1,181 @@ + +import kdom, jdict, jstrutils + +type + Message = enum + Unchanged + Changed + Mark + Inserted + Deleted + + State = object + stale: int + outdated: bool + phantom: bool + +type + ReactiveBase* = ref object of RootObj ## everything that is a "reactive" + ## value derives from that + sinks*: seq[proc(msg: Message, pos: int)] + Reactive*[T] = ref object of ReactiveBase + value*: T + + RString* = Reactive[cstring] +# RTime* = Reactive[Time] + RInt* = Reactive[int] + + RBool* = Reactive[bool] + + RSeq*[T] = ref object of ReactiveBase + s: seq[T] + L: RInt + +proc addSink[T](x: Reactive[T]; sink: proc(msg: Message; pos: int)) = + x.sinks.add sink + +proc broadcast(x: ReactiveBase, msg: Message; pos = 0) = + for s in x.sinks: s(msg, pos) + +var toTrack: proc (msg: Message; pos: int) = nil + +proc now[T](x: Reactive[T]): T = + if toTrack != nil: + x.addSink toTrack + result = x.value + +proc `:=`[T](x: Reactive[T], f: proc(): T) = + var state: State + + toTrack = proc(msg: Message; pos: int) = + if not state.phantom: + case msg.kind: + of Changed, Unchanged: + state.outdated = state.outdated or (msg.kind == Changed) + dec state.stale + if state.stale == 0: + if state.outdated: + var t = f() + let thisChanged = if x.value != t: Changed else: Unchanged + x.value = t + x.broadcast(thisChanged, pos) + state.outdated = false + else: + x.broadcast(Unchanged, pos) + of Mark: + if state.stale == 0: + x.broadcast(msg, pos) + inc state.stale + of Inserted, Deleted: + dec state.stale + if state.stale == 0: + x.broadcast(msg, pos) + #state.phantom = true + + x.value = f() + toTrack = nil + +proc `<-`[T](x: Reactive[T], val: T) = + if x.value != val: + x.value = val + x.broadcast(Mark) + x.broadcast(Changed) + +proc changed*(x: ReactiveBase) = + x.broadcast(Mark) + x.broadcast(Changed) + +proc subscribe[T](x: Reactive[T], f: proc(x: T)) = + let reactor = proc (msg: Message, pos: int) = + case msg: + of Mark: discard + of Changed, Unchanged: f(x.value) + of Inserted, Deleted: discard + x.addSink reactor + +template lift1(op: untyped) = + proc op[T](a: Reactive[T]): (proc(): T) = + result = proc(): T = op(a.now) + +template lift2(op: untyped) = + proc op[T](a, b: Reactive[T]): (proc(): T) = + result = proc(): T = op(a.now, b.now) + +lift1 `not` +lift2 `&` + +proc newReactive[T](x: T): Reactive[T] = + result = Reactive[T](value: x) + +proc rstr(x: cstring): RString = + result = RString(value: x) + +proc newRSeq*[T](len: int): RSeq[T] = + result = RSeq[T](s: newSeq[T](len), L: newReactive[int](0)) + +proc newRSeq*[T](data: seq[T]): RSeq[T] = + result = RSeq[T](s: newSeq[T](data.len), L: newReactive[int](0)) + for i in 0..high(data): + result.s[i] = data[i] + +proc `[]=`[T](x: RSeq[T]; index: int; v: T) = + x.s[index] = v + +proc `[]`[T](x: RSeq[T]; index: int): T = x.s[index] + +proc insert*[T](x: RSeq[T]; y: T; position = 0) = + x.s.insert(y, position) + x.broadcast(Mark) + x.broadcast(Inserted, position) + +proc delete*[T](x: RSeq[T]; position = 0) = + x.s.delete(position) + x.broadcast(Mark) + x.broadcast(Deleted, position) + +proc map[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] = + let xl = x.L.value + let res = newRSeq[U](xl) + for i in 0.. Date: Thu, 17 Aug 2017 08:36:17 +0200 Subject: [PATCH 102/247] nextgen uses the karax DSL and DOM diffing --- experiments/nextgen.nim | 28 ++++++++++------------------ src/karax.nim | 1 + 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/experiments/nextgen.nim b/experiments/nextgen.nim index eb94658..8df39a2 100644 --- a/experiments/nextgen.nim +++ b/experiments/nextgen.nim @@ -1,5 +1,5 @@ -import kdom, jdict, jstrutils +import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils type Message = enum @@ -155,27 +155,19 @@ type var gu = newReactive(User(firstname: "Some", lastname: "Body")) -proc renderUser(u: Reactive[User]): Node = - result = document.createElement("button") - result.appendChild document.createTextNode(u.now.firstname & " " & u.now.lastname) - result.addEventListener "click", proc (ev: Event) = - gu <- User(firstname: "Another", lastname: "Guy") +proc renderUser(u: Reactive[User]): VNode = + result = buildHtml(button): + text u.now.firstname & " " & u.now.lastname + proc onclick(ev: Event; n: VNode) = + gu <- User(firstname: "Another", lastname: "Guy") -proc replaceById(id: cstring; newTree: Node) = - let x = document.getElementById(id) - x.parentNode.replaceChild(newTree, x) - newTree.id = id - -template track(r: ReactiveBase; a, b: Node) = +template track(r: ReactiveBase; a, b: VNode) = r.addSink proc(m: Message; pos: int) = if m == Changed: - a.parentNode.replaceChild(b, a) + runDiff(kxi, a, b) -proc main(): Node = +proc main(): VNode = result = renderUser(gu) track gu, result, renderUser(gu) -proc init(ev: Event) = - replaceById("ROOT", main()) - -window.onload = init +setInitializer(main) diff --git a/src/karax.nim b/src/karax.nim index b0b7761..3f53405 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -187,6 +187,7 @@ proc same(n: VNode, e: Node; nesting = 0): bool = proc replaceById(id: cstring; newTree: Node) = let x = document.getElementById(id) x.parentNode.replaceChild(newTree, x) + newTree.id = id type EqResult = enum From 46b12594984d571a0a19e2e8c9f98fc75f238c62 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 17 Aug 2017 11:28:11 +0200 Subject: [PATCH 103/247] added .track macro --- experiments/nextgen.nim | 170 +++---------------------------------- src/reactive.nim | 182 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 159 deletions(-) create mode 100644 src/reactive.nim diff --git a/experiments/nextgen.nim b/experiments/nextgen.nim index 8df39a2..fb95a6b 100644 --- a/experiments/nextgen.nim +++ b/experiments/nextgen.nim @@ -1,173 +1,25 @@ -import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils - -type - Message = enum - Unchanged - Changed - Mark - Inserted - Deleted - - State = object - stale: int - outdated: bool - phantom: bool - -type - ReactiveBase* = ref object of RootObj ## everything that is a "reactive" - ## value derives from that - sinks*: seq[proc(msg: Message, pos: int)] - Reactive*[T] = ref object of ReactiveBase - value*: T - - RString* = Reactive[cstring] -# RTime* = Reactive[Time] - RInt* = Reactive[int] - - RBool* = Reactive[bool] - - RSeq*[T] = ref object of ReactiveBase - s: seq[T] - L: RInt - -proc addSink[T](x: Reactive[T]; sink: proc(msg: Message; pos: int)) = - x.sinks.add sink - -proc broadcast(x: ReactiveBase, msg: Message; pos = 0) = - for s in x.sinks: s(msg, pos) - -var toTrack: proc (msg: Message; pos: int) = nil - -proc now[T](x: Reactive[T]): T = - if toTrack != nil: - x.addSink toTrack - result = x.value - -proc `:=`[T](x: Reactive[T], f: proc(): T) = - var state: State - - toTrack = proc(msg: Message; pos: int) = - if not state.phantom: - case msg.kind: - of Changed, Unchanged: - state.outdated = state.outdated or (msg.kind == Changed) - dec state.stale - if state.stale == 0: - if state.outdated: - var t = f() - let thisChanged = if x.value != t: Changed else: Unchanged - x.value = t - x.broadcast(thisChanged, pos) - state.outdated = false - else: - x.broadcast(Unchanged, pos) - of Mark: - if state.stale == 0: - x.broadcast(msg, pos) - inc state.stale - of Inserted, Deleted: - dec state.stale - if state.stale == 0: - x.broadcast(msg, pos) - #state.phantom = true - - x.value = f() - toTrack = nil - -proc `<-`[T](x: Reactive[T], val: T) = - if x.value != val: - x.value = val - x.broadcast(Mark) - x.broadcast(Changed) - -proc changed*(x: ReactiveBase) = - x.broadcast(Mark) - x.broadcast(Changed) - -proc subscribe[T](x: Reactive[T], f: proc(x: T)) = - let reactor = proc (msg: Message, pos: int) = - case msg: - of Mark: discard - of Changed, Unchanged: f(x.value) - of Inserted, Deleted: discard - x.addSink reactor - -template lift1(op: untyped) = - proc op[T](a: Reactive[T]): (proc(): T) = - result = proc(): T = op(a.now) - -template lift2(op: untyped) = - proc op[T](a, b: Reactive[T]): (proc(): T) = - result = proc(): T = op(a.now, b.now) - -lift1 `not` -lift2 `&` - -proc newReactive[T](x: T): Reactive[T] = - result = Reactive[T](value: x) - -proc rstr(x: cstring): RString = - result = RString(value: x) - -proc newRSeq*[T](len: int): RSeq[T] = - result = RSeq[T](s: newSeq[T](len), L: newReactive[int](0)) - -proc newRSeq*[T](data: seq[T]): RSeq[T] = - result = RSeq[T](s: newSeq[T](data.len), L: newReactive[int](0)) - for i in 0..high(data): - result.s[i] = data[i] - -proc `[]=`[T](x: RSeq[T]; index: int; v: T) = - x.s[index] = v - -proc `[]`[T](x: RSeq[T]; index: int): T = x.s[index] - -proc insert*[T](x: RSeq[T]; y: T; position = 0) = - x.s.insert(y, position) - x.broadcast(Mark) - x.broadcast(Inserted, position) - -proc delete*[T](x: RSeq[T]; position = 0) = - x.s.delete(position) - x.broadcast(Mark) - x.broadcast(Deleted, position) - -proc map[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] = - let xl = x.L.value - let res = newRSeq[U](xl) - for i in 0.. Date: Sat, 19 Aug 2017 14:54:10 +0200 Subject: [PATCH 104/247] reactive sequence works now --- experiments/nextgen.nim | 67 ++++++++++++++++++++++++++++--------- src/karax.nim | 18 ++++++++++ src/reactive.nim | 73 +++++++++++++++++++++++++++++++++++------ src/vdom.nim | 4 +++ 4 files changed, 137 insertions(+), 25 deletions(-) diff --git a/experiments/nextgen.nim b/experiments/nextgen.nim index fb95a6b..4f955aa 100644 --- a/experiments/nextgen.nim +++ b/experiments/nextgen.nim @@ -2,24 +2,61 @@ import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, reactive type - User = ref object + User = ref object of ReactiveBase firstname, lastname: cstring -var gu = @[newReactive(User(firstname: "Some", lastname: "Body")), - newReactive(User(firstname: "Some", lastname: "One")), - newReactive(User(firstname: "Some", lastname: "Two"))] +var gu = newRSeq(@[ (User(firstname: "Some", lastname: "Body")), + (User(firstname: "Some", lastname: "One")), + (User(firstname: "Some", lastname: "Two"))]) var clicks = 0 -proc renderUser(u: Reactive[User]): VNode {.track.} = - result = buildHtml(button): - text u.now.firstname & " " & u.now.lastname - proc onclick(ev: Event; n: VNode) = - inc clicks - u <- User(firstname: "Another", lastname: &clicks) +discard """ + # Text gets a *reactive* string in the first place! + # Text can register and knows how to update itself! + proc toReact(): RString = + observe(u): + u.firstname & u.lastname -proc main(): VNode = - result = buildHtml(tdiv): - for i in 0..high(gu): - renderUser(gu[i]) + let t = text(u.firstname & " " & u.lastname) + observe(u, t.update(u.firstname & " " & u.lastname)) + t +""" -setInitializer(main) +proc renderUser(u: User): VNode = #{.track.} = + proc inner(u: User): VNode = + result = buildHtml(tdiv): + text u.firstname & " " & u.lastname + button: + text "(X)" + proc onclick(ev: Event; n: VNode) = + #u.firstname = "kfdj" + #u.lastname = &clicks + #notifyObservers(u) + gu.deleteElem(u) + + result = inner(u) + doTrack(u, result, inner(u)) + +template vmap(x: RSeq; elem, f: untyped): VNode = + let tmp = buildHtml(elem): + for i in 0..= parent.len: + kxi.addPatch(pkAppend, current, nil, kid) + parent.add(kid) + else: + let before = current.childNodes[position] + kxi.addPatch(pkInsertBefore, current, before, kid) + parent.insert(kid, position) + applyPatch(kxi) + proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) = let olddom = oldNode.dom discard diff(newNode, oldNode, nil, olddom, kxi) diff --git a/src/reactive.nim b/src/reactive.nim index a1782f0..4fcc851 100644 --- a/src/reactive.nim +++ b/src/reactive.nim @@ -32,7 +32,7 @@ type s: seq[T] L: RInt -proc addSink[T](x: Reactive[T]; sink: proc(msg: Message; pos: int)) = +proc addSink(x: ReactiveBase; sink: proc(msg: Message; pos: int)) = x.sinks.add sink proc addSink(x: ReactiveBase; key: cstring; sink: proc(msg: Message; pos: int)) = @@ -88,7 +88,7 @@ proc `<-`*[T](x: Reactive[T], val: T) = x.broadcast(Mark) x.broadcast(Changed) -proc changed*(x: ReactiveBase) = +proc notifyObservers*(x: ReactiveBase) = x.broadcast(Mark) x.broadcast(Changed) @@ -125,10 +125,17 @@ proc newRSeq*[T](data: seq[T]): RSeq[T] = for i in 0..high(data): result.s[i] = data[i] -proc `[]=`[T](x: RSeq[T]; index: int; v: T) = +proc `[]=`*[T](x: RSeq[T]; index: int; v: T) = x.s[index] = v -proc `[]`[T](x: RSeq[T]; index: int): T = x.s[index] +proc `[]`*[T](x: RSeq[T]; index: int): T = x.s[index] +proc len*[T](x: RSeq[T]): int = x.s.len + +proc add*[T](x: RSeq[T]; y: T) = + let position = x.s.len + x.s.add(y) + x.broadcast(Mark) + x.broadcast(Inserted, position) proc insert*[T](x: RSeq[T]; y: T; position = 0) = x.s.insert(y, position) @@ -140,6 +147,16 @@ proc delete*[T](x: RSeq[T]; position = 0) = x.broadcast(Mark) x.broadcast(Deleted, position) +proc deleteElem*[T](x: RSeq[T]; y: T) = + var position = -1 + for i in 0..= 0: + x.s.delete(position) + x.broadcast(Mark) + x.broadcast(Deleted, position) + proc map*[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] = let xl = x.L.value let res = newRSeq[U](xl) @@ -159,24 +176,60 @@ proc map*[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] = import macros template trackImpl(r: ReactiveBase; key: cstring; a, b: untyped) = - when r is Reactive: + when r is ReactiveBase: addSink r, key, proc(m: Message; pos: int) = if m == Changed: runDiff(kxi, a, b) +template doTrack*(r: ReactiveBase; a, b: untyped) {.dirty.} = + bind addSink, Message, RSeq, Changed, Deleted, Inserted + addSink r, proc(m: Message; pos: int) = + #when r is RSeq: + #echo "Message: ", m, " ", pos + if m == Changed: karax.runDiff(kxi, a, b) + +template doTrackResize*(r: ReactiveBase; a, b: untyped) {.dirty.} = + bind addSink, Message, RSeq, Changed, Deleted, Inserted + addSink r, proc(m: Message; pos: int) = + #when r is RSeq: + #echo "Message: ", m, " ", pos + case m + of Deleted: karax.runDel(kxi, a, pos) + of Inserted: + karax.runIns(kxi, a, b, pos) + else: discard + +template notifyImpl(r: untyped) = + when r is ReactiveBase: + notifyObservers(r) + +proc root(n: NimNode): NimNode = + result = n + while result.kind in {nnkDotExpr, nnkBracketExpr}: result = result[0] + +proc analyse(n: NimNode; paramList: seq[NimNode]): NimNode = + result = n + #if n.kind in {nnkAsgn, nnkFastAsgn}: + # + # n[0] + #else: + # recurse() + macro track*(procDef: untyped): untyped = let params = params(procDef) let key = lineInfo(procDef) var call = newCall(procDef.name) var trackings = newStmtList() - for b in procDef.body: trackings.add b + var paramList: seq[NimNode] = @[] for j in 1.. Date: Tue, 22 Aug 2017 18:04:49 +0200 Subject: [PATCH 105/247] wip; nothing works --- experiments/nextgen.nim | 109 +++++++++++++++++++++++++++++++--------- src/reactive.nim | 17 +++++-- src/vdom.nim | 11 +++- 3 files changed, 107 insertions(+), 30 deletions(-) diff --git a/experiments/nextgen.nim b/experiments/nextgen.nim index 4f955aa..d311eb9 100644 --- a/experiments/nextgen.nim +++ b/experiments/nextgen.nim @@ -1,6 +1,25 @@ import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, reactive +proc newTextInput*(text: RString; focus: RBool): VNode = + proc onFlip(ev: Event; target: VNode) = + focus <- not focus.value + + proc onKeyupEnter(ev: Event; target: VNode) = + text <- target.value + #text.notifyObservers() + + proc onkeyup(ev: Event; n: VNode) = + # keep displayValue up to date, but do not tell the client yet! + text.value = n.value + + result = buildHtml(input(`type`="text", + value=text.value, onblur=onFlip, onfocus=onFlip, + onkeyupenter=onkeyupenter, onkeyup=onkeyup, setFocus=focus.value)) + +var + errmsg = rstr("") + type User = ref object of ReactiveBase firstname, lastname: cstring @@ -8,6 +27,43 @@ type var gu = newRSeq(@[ (User(firstname: "Some", lastname: "Body")), (User(firstname: "Some", lastname: "One")), (User(firstname: "Some", lastname: "Two"))]) +var selected = newReactive[User](nil) + +proc toUI*(): RString = + result = RString() + result.subscribe proc (v: cstring) = + if v.len > 0: + if selected >= 0: + # XXX what's happening here? + persons[selected].firstname = v + notifyObservers(persons[selected]) + errmsg <- "" + else: + errmsg <- "name must not be empty" + +var inp = toUI() + +proc adaptFocus(def = false): RBool = + result = RBool() + result.value = def + result.subscribe proc (hasFocus: bool) = + if not hasFocus: + inp.notifyObservers() + +var focus = adaptFocus() + +proc styler(): VStyle = + result = style( + (StyleAttr.position, cstring"relative"), + (StyleAttr.paddingLeft, cstring"10px"), + (StyleAttr.paddingRight, cstring"5px"), + (StyleAttr.height, cstring"30px"), + (StyleAttr.lineHeight, cstring"30px"), + (StyleAttr.border, cstring"solid 8px " & (if focus.value: cstring"red" else: cstring"black")), + (StyleAttr.fontSize, cstring"12px"), + (StyleAttr.fontWeight, cstring"600") + ) + var clicks = 0 discard """ @@ -22,20 +78,23 @@ discard """ t """ -proc renderUser(u: User): VNode = #{.track.} = - proc inner(u: User): VNode = - result = buildHtml(tdiv): - text u.firstname & " " & u.lastname - button: - text "(X)" - proc onclick(ev: Event; n: VNode) = - #u.firstname = "kfdj" - #u.lastname = &clicks - #notifyObservers(u) - gu.deleteElem(u) +template observe(s: cstring): RString = + let tmp = rstr(s) + u.subscribeSelf proc () = + tmp <- s + temp - result = inner(u) - doTrack(u, result, inner(u)) +proc renderUser(u: User; selected: RBool): VNode = + result = buildHtml(tdiv): + let displayName = observe(u.firstname & " " & u.lastname) + if selected == u: + newTextInput displayName, focus + else: + text displayName + button: + text "(x)" + proc onclick(ev: Event; n: VNode) = + gu.deleteElem(u) template vmap(x: RSeq; elem, f: untyped): VNode = let tmp = buildHtml(elem): @@ -44,18 +103,18 @@ template vmap(x: RSeq; elem, f: untyped): VNode = doTrackResize(x, tmp, f(x[pos])) tmp -proc main(gu: RSeq[User]): VNode = #{.track.} = - proc inner(gu: RSeq[User]): VNode = - result = buildHtml(tdiv): - vmap(gu, tdiv, renderUser) - tdiv: - button: - text "Add User" - proc onclick(ev: Event; n: VNode) = - inc clicks - gu.add User(firstname: "Added", lastname: &clicks) - result = inner(gu) - doTrack(gu, result, inner(gu)) +proc main(gu: RSeq[User]): VNode = + result = buildHtml(tdiv): + tdiv: + button: + text "Add User" + proc onclick(ev: Event; n: VNode) = + inc clicks + gu.add User(firstname: "Added", lastname: &clicks) + tdiv: + text errmsg + vmap(gu, tdiv, renderUser) + proc init(): VNode = main(gu) diff --git a/src/reactive.nim b/src/reactive.nim index 4fcc851..a0944a9 100644 --- a/src/reactive.nim +++ b/src/reactive.nim @@ -8,6 +8,7 @@ type Mark Inserted Deleted + Replaced State = object stale: int @@ -70,7 +71,7 @@ template wrapObserver(f: untyped) = of Mark: if state.stale == 0: x.broadcast(msg, pos) inc state.stale - of Inserted, Deleted: + of Inserted, Deleted, Replaced: dec state.stale if state.stale == 0: x.broadcast(msg, pos) @@ -92,12 +93,20 @@ proc notifyObservers*(x: ReactiveBase) = x.broadcast(Mark) x.broadcast(Changed) -proc subscribe[T](x: Reactive[T], f: proc(x: T)) = +proc subscribeVal*[T](x: Reactive[T], f: proc(x: T)) = let reactor = proc (msg: Message, pos: int) = case msg: of Mark: discard of Changed, Unchanged: f(x.value) - of Inserted, Deleted: discard + of Inserted, Deleted, Replaced: discard + x.addSink reactor + +proc subscribeSelf*[T: ReactiveBase](x: T, f: proc()) = + let reactor = proc (msg: Message, pos: int) = + case msg: + of Mark: discard + of Changed, Unchanged: f() + of Inserted, Deleted, Replaced: discard x.addSink reactor template lift1(op: untyped) = @@ -127,6 +136,8 @@ proc newRSeq*[T](data: seq[T]): RSeq[T] = proc `[]=`*[T](x: RSeq[T]; index: int; v: T) = x.s[index] = v + x.broadcast(Mark) + x.broadcast(Replaced, index) proc `[]`*[T](x: RSeq[T]; index: int): T = x.s[index] proc len*[T](x: RSeq[T]): int = x.s.len diff --git a/src/vdom.nim b/src/vdom.nim index bf84cb8..63f6669 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -239,8 +239,15 @@ proc tree*(kind: VNodeKind; attrs: openarray[(cstring, cstring)]; result = tree(kind, kids) for a in attrs: result.setAttr(a[0], a[1]) -proc text*(s: string): VNode = VNode(kind: VNodeKind.text, text: cstring(s), index: -1) -proc text*(s: cstring): VNode = VNode(kind: VNodeKind.text, text: s, index: -1) +proc rawtext*(s: string): VNode = VNode(kind: VNodeKind.text, text: cstring(s), index: -1) +proc rawtext*(s: cstring): VNode = VNode(kind: VNodeKind.text, text: s, index: -1) + +import reactive + +proc text*(s: RString): VNode = + result = VNode(kind: VNodeKind.text, text: s, index: -1) + s.subscribe proc(v: cstring) = + result.dom.value = v iterator items*(n: VNode): VNode = for i in 0.. Date: Wed, 23 Aug 2017 19:35:27 +0200 Subject: [PATCH 106/247] more reactive stuff works now --- experiments/nextgen.nim | 55 +++++++++++++++++++++++++-------- src/karax.nim | 9 ++++-- src/reactive.nim | 67 +++++++++++++++++------------------------ src/vdom.nim | 25 +++++++++------ 4 files changed, 94 insertions(+), 62 deletions(-) diff --git a/experiments/nextgen.nim b/experiments/nextgen.nim index d311eb9..40acf04 100644 --- a/experiments/nextgen.nim +++ b/experiments/nextgen.nim @@ -1,7 +1,7 @@ import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, reactive -proc newTextInput*(text: RString; focus: RBool): VNode = +proc newTextInput*(text: RString; focus: RBool): VNode {.track.} = proc onFlip(ev: Event; target: VNode) = focus <- not focus.value @@ -23,20 +23,22 @@ var type User = ref object of ReactiveBase firstname, lastname: cstring + selected: bool var gu = newRSeq(@[ (User(firstname: "Some", lastname: "Body")), (User(firstname: "Some", lastname: "One")), (User(firstname: "Some", lastname: "Two"))]) -var selected = newReactive[User](nil) +var prevSelected: User = nil #newReactive[User](nil) proc toUI*(): RString = result = RString() result.subscribe proc (v: cstring) = if v.len > 0: - if selected >= 0: + let p = prevSelected #selected.value + if p != nil: # XXX what's happening here? - persons[selected].firstname = v - notifyObservers(persons[selected]) + p.firstname = v + notifyObservers(p) errmsg <- "" else: errmsg <- "name must not be empty" @@ -76,20 +78,31 @@ discard """ let t = text(u.firstname & " " & u.lastname) observe(u, t.update(u.firstname & " " & u.lastname)) t -""" template observe(s: cstring): RString = let tmp = rstr(s) u.subscribeSelf proc () = tmp <- s temp +""" -proc renderUser(u: User; selected: RBool): VNode = +proc renderUser(u: User): VNode {.track.} = result = buildHtml(tdiv): - let displayName = observe(u.firstname & " " & u.lastname) - if selected == u: - newTextInput displayName, focus + let displayName = u.firstname & " " & u.lastname + if u.selected: + # == selected.value: + !(inp <- displayName) + newTextInput inp, focus else: + button: + text "..." + proc onclick(ev: Event; n: VNode) = + if prevSelected != nil: + prevSelected.selected = false + notifyObservers(prevSelected) + u.selected = true + notifyObservers(u) + prevSelected = u text displayName button: text "(x)" @@ -103,6 +116,25 @@ template vmap(x: RSeq; elem, f: untyped): VNode = doTrackResize(x, tmp, f(x[pos])) tmp +template vmapIt(x: RSeq; elem, call: untyped): VNode = + var it {.inject}: type(x[0]) + let tmp = buildHtml(elem): + for i in 0.. 0: + # x.sinks[0] = sink + #else: + x.sinks.add sink + if x.id == 0: + inc rid + x.id = rid proc broadcast(x: ReactiveBase, msg: Message; pos = 0) = for s in x.sinks: s(msg, pos) @@ -93,7 +93,7 @@ proc notifyObservers*(x: ReactiveBase) = x.broadcast(Mark) x.broadcast(Changed) -proc subscribeVal*[T](x: Reactive[T], f: proc(x: T)) = +proc subscribe*[T](x: Reactive[T], f: proc(x: T)) = let reactor = proc (msg: Message, pos: int) = case msg: of Mark: discard @@ -186,11 +186,10 @@ proc map*[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] = import macros -template trackImpl(r: ReactiveBase; key: cstring; a, b: untyped) = +template trackImpl(r: ReactiveBase; a, b: untyped) = when r is ReactiveBase: - addSink r, key, proc(m: Message; pos: int) = - if m == Changed: - runDiff(kxi, a, b) + addSink r, proc(m: Message; pos: int) = + if m == Changed: karax.runDiff(kxi, a, b) template doTrack*(r: ReactiveBase; a, b: untyped) {.dirty.} = bind addSink, Message, RSeq, Changed, Deleted, Inserted @@ -207,40 +206,30 @@ template doTrackResize*(r: ReactiveBase; a, b: untyped) {.dirty.} = case m of Deleted: karax.runDel(kxi, a, pos) of Inserted: + let it {.used.} = r[pos] karax.runIns(kxi, a, b, pos) else: discard -template notifyImpl(r: untyped) = - when r is ReactiveBase: - notifyObservers(r) - -proc root(n: NimNode): NimNode = - result = n - while result.kind in {nnkDotExpr, nnkBracketExpr}: result = result[0] - -proc analyse(n: NimNode; paramList: seq[NimNode]): NimNode = - result = n - #if n.kind in {nnkAsgn, nnkFastAsgn}: - # - # n[0] - #else: - # recurse() - macro track*(procDef: untyped): untyped = let params = params(procDef) - let key = lineInfo(procDef) - var call = newCall(procDef.name) var trackings = newStmtList() - var paramList: seq[NimNode] = @[] + var inner = copyNimTree(procDef) + inner[0] = ident($procDef.name & "Inner") + var call = newCall(inner[0]) + trackings.add inner + trackings.add newAssignment(ident"result", call) + for j in 1.. Date: Mon, 28 Aug 2017 11:29:12 +0200 Subject: [PATCH 107/247] improvments to the new local diff algorithm --- src/karax.nim | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index f68e3ff..2df1d9f 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -182,7 +182,7 @@ proc same(n: VNode, e: Node; nesting = 0): bool = for i in 0 ..< n.len: if not same(n[i], e[i], nesting+1): return false else: - kout toTag[n.kind], e.nodename + echo "VDOM: ", toTag[n.kind], " DOM: ", e.nodename proc replaceById(id: cstring; newTree: Node) = let x = document.getElementById(id) @@ -334,7 +334,8 @@ proc applyPatch(kxi: KaraxInstance) = if n.kind == VNodeKind.component: let x = VComponent(n) if x.onDetachImpl != nil: x.onDetachImpl(x) - n.dom = nil + # XXX for some reason this causes assertion errors otherwise: + if not kxi.surpressRedraws: n.dom = nil kxi.patchLen = 0 for i in 0.. Date: Mon, 28 Aug 2017 11:29:43 +0200 Subject: [PATCH 108/247] created a nice 'makeReactive' macro --- src/reactive.nim | 128 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 119 insertions(+), 9 deletions(-) diff --git a/src/reactive.nim b/src/reactive.nim index 307d317..34c630e 100644 --- a/src/reactive.nim +++ b/src/reactive.nim @@ -1,4 +1,6 @@ +import jdict + type Message = enum Unchanged @@ -14,9 +16,11 @@ type phantom: bool type + SinkSeq = seq[proc(msg: Message, pos: int)] ReactiveBase* = ref object of RootObj ## everything that is a "reactive" ## value derives from that - sinks*: seq[proc(msg: Message, pos: int)] + sinks*: SinkSeq + dups: JDict[cstring, bool] id: int Reactive*[T] = ref object of ReactiveBase value*: T @@ -33,10 +37,16 @@ type var rid: int +proc addSink(x: ReactiveBase; key: cstring; sink: proc(msg: Message; pos: int)) = + if x.dups == nil: x.dups = newJDict[cstring, bool]() + if not x.dups.contains(key): + x.dups[key] = true + x.sinks.add sink + if x.id == 0: + inc rid + x.id = rid + proc addSink(x: ReactiveBase; sink: proc(msg: Message; pos: int)) = - #if x.sinks.len > 0: - # x.sinks[0] = sink - #else: x.sinks.add sink if x.id == 0: inc rid @@ -78,6 +88,29 @@ template wrapObserver(f: untyped) = #state.phantom = true helper +template glitchFree(f: untyped) = + var state: State + proc helper(msg: Message; pos: int) = + case msg.kind: + of Changed, Unchanged: + state.outdated = state.outdated or (msg.kind == Changed) + dec state.stale + if state.stale == 0: + if state.outdated: + x.value = f + x.broadcast(msg.kind, pos) + state.outdated = false + else: + x.broadcast(Unchanged, pos) + of Mark: + if state.stale == 0: x.broadcast(msg, pos) + inc state.stale + of Inserted, Deleted, Replaced: + dec state.stale + if state.stale == 0: + x.broadcast(msg, pos) + helper + proc `:=`[T](x: Reactive[T], f: proc(): T) = toTrack = wrapObserver(f()) x.value = f() @@ -186,17 +219,29 @@ proc map*[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] = import macros -template trackImpl(r: ReactiveBase; a, b: untyped) = +template protect(r: ReactiveBase; body: untyped) = + var tmp: seq[proc(msg: Message, pos: int)] + swap(r.sinks, tmp) + body + swap(r.sinks, tmp) + +template trackImpl(r: ReactiveBase; key: cstring; a, b: untyped) = when r is ReactiveBase: - addSink r, proc(m: Message; pos: int) = - if m == Changed: karax.runDiff(kxi, a, b) + addSink r, key, proc(m: Message; pos: int) = + if m == Changed: + protect r: + echo "runDiff A" + karax.runDiff(kxi, a, b) template doTrack*(r: ReactiveBase; a, b: untyped) {.dirty.} = bind addSink, Message, RSeq, Changed, Deleted, Inserted addSink r, proc(m: Message; pos: int) = #when r is RSeq: #echo "Message: ", m, " ", pos - if m == Changed: karax.runDiff(kxi, a, b) + if m == Changed: + protect r: + echo "runDiff B" + karax.runDiff(kxi, a, b) template doTrackResize*(r: ReactiveBase; a, b: untyped) {.dirty.} = bind addSink, Message, RSeq, Changed, Deleted, Inserted @@ -226,10 +271,75 @@ macro track*(procDef: untyped): untyped = let param = x[i] call.add(param) + let key = lineInfo(procDef) for j in 1..