From 6c152ed516b2154fc16b26248676be9aa5601533 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 10 Jul 2017 21:42:22 +0200 Subject: [PATCH 01/28] 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 02/28] 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 03/28] 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 04/28] 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 05/28] 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 06/28] 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 07/28] 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 08/28] 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 09/28] 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 10/28] 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 11/28] 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 12/28] 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 13/28] 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 14/28] 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 15/28] 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 16/28] 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 17/28] 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 18/28] 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 19/28] 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..