diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5e6c415 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +# This is a basic workflow to help you get started with Actions + +name: CI + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the master branch + push: + branches: [ master ] + pull_request: + branches: [ master ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + container: nimlang/nim + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + # Runs a single command using the runners shell + - name: Tests + run: | + nimble develop -y + nim c -r tests/tester.nim diff --git a/.gitignore b/.gitignore index 67d9b34..842e140 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ nimcache/ +/tests/tester +karax/tools/karun +*.code-workspace +*.exe +app.js +app.html diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..ac49796 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Xored Software, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/docs/benchmark.png b/docs/benchmark.png new file mode 100644 index 0000000..3949ad5 Binary files /dev/null and b/docs/benchmark.png differ diff --git a/docs/docs.rst b/docs/docs.rst new file mode 100644 index 0000000..3eec1a4 --- /dev/null +++ b/docs/docs.rst @@ -0,0 +1,58 @@ + +Virtual DOM +=========== + +The virtual dom is in the ``vdom`` module. + +The ``VNodeKind`` enum describes every tag that the HTML 5 spec includes +and some extensions that allow for an efficient component system. + +These extensions are: + +``VNodeKind.int`` + The node has a single integer field. + +``VNodeKind.bool`` + The node has a single boolean field. + +``VNodeKind.vthunk`` + The node is a `virtual thunk`:idx:. This means there is a + function attached to it that produces the ``VNode`` structure + on demand. + +``VNodeKind.dthunk`` + The node is a `DOM thunk`:idx:. This means there is a + function attached to it that produces the ``Node`` DOM structure + on demand. + +For convenience of the resulting Nim DSL these tags have enum names +that differ from their HTML equivalent: + +================= ======================================================= +Enum value HTML +================= ======================================================= +``tdiv`` ``div`` (changed because ``div`` is a keyword in Nim) +``italic`` ``i`` +``bold`` ``b`` +``strikethrough`` ``s`` +``underlined`` ``u`` +================= ======================================================= + + +A virtual dom node also has a special field set called ``key``, an integer +that can be used as a data model specific key/id. It can be accessed in event +handlers to change the data model. See the todoapp for an example of how to +use it. + +**Note**: This is not a hint for the DOM diff algorithm, multipe nodes can +all have the same key value. + + +Event system +============ + +Karax does not abstract over the event system the DOM offers much: The same +``Event`` structure is used. Every callback has the +signature ``proc (ev: Event; n: VNode)`` or empty ``proc ()``. + + diff --git a/examples/button.nim b/examples/button.nim new file mode 100644 index 0000000..d86fa82 --- /dev/null +++ b/examples/button.nim @@ -0,0 +1,15 @@ +include karax / prelude + +var lines: seq[kstring] = @[] + +proc createDom(): VNode = + result = buildHtml(tdiv): + button: + text "Say hello!" + proc onclick(ev: Event; n: VNode) = + lines.add "Hello simulated universe" + for x in lines: + tdiv: + text x + +setRenderer createDom diff --git a/examples/buttonlambda.nim b/examples/buttonlambda.nim new file mode 100644 index 0000000..2ddadff --- /dev/null +++ b/examples/buttonlambda.nim @@ -0,0 +1,14 @@ +include karax / prelude +from future import `=>` + +var lines: seq[kstring] = @[] + +proc createDom(): VNode = + result = buildHtml(tdiv): + button(onclick = () => lines.add "Hello simulated universe"): + text "Say hello!" + for x in lines: + tdiv: + text x + +setRenderer createDom diff --git a/examples/carousel/carousel.html b/examples/carousel/carousel.html new file mode 100644 index 0000000..500e73a --- /dev/null +++ b/examples/carousel/carousel.html @@ -0,0 +1,15 @@ + + + + + Carousel app + + + + +
+ + + + + diff --git a/examples/carousel/carousel.nim b/examples/carousel/carousel.nim new file mode 100644 index 0000000..c2db56c --- /dev/null +++ b/examples/carousel/carousel.nim @@ -0,0 +1,69 @@ +## This demo shows how you can develop your own stateful components with Karax. + +import vdom, vstyles, karax, karaxdsl, jdict, jstrutils, kdom + +type + Carousel = ref object of VComponent + counter: int + cntdown: int + timer: TimeOut + list: seq[cstring] + +const ticksUntilChange = 5 + +var + images: seq[cstring] = @[cstring"a", "b", "c", "d"] + +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) + markDirty(self) + redraw() + + proc onclick(ev: Event; n: VNode) = + if self.timer != nil: + clearTimeout(self.timer) + self.timer = setTimeout(docount, 30) + + let col = + 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()): + text self.list[self.counter] + button(onclick = onclick): + text "Next" + tdiv(style = style(StyleAttr.color, col)): + text "This changes its color." + if self.cntdown != ticksUntilChange: + text &self.cntdown + +proc carousel(): Carousel = + result = newComponent(Carousel, render) + result.list = images + result.cntdown = ticksUntilChange + +proc createDom(): VNode = + result = buildHtml(table): + tr: + td: + carousel() + td: + carousel() + tr: + td: + carousel() + td: + carousel() + +setRenderer createDom diff --git a/examples/carousel/carousel.nims b/examples/carousel/carousel.nims new file mode 100644 index 0000000..cef2dec --- /dev/null +++ b/examples/carousel/carousel.nims @@ -0,0 +1 @@ +--path: "../../karax" diff --git a/examples/config.nims b/examples/config.nims new file mode 100644 index 0000000..76ed4d9 --- /dev/null +++ b/examples/config.nims @@ -0,0 +1 @@ +--path: ".." diff --git a/examples/hellostyle.nim b/examples/hellostyle.nim new file mode 100644 index 0000000..716c2f0 --- /dev/null +++ b/examples/hellostyle.nim @@ -0,0 +1,17 @@ +include karax / prelude +import karax / vstyles + +proc createDom(): VNode = + result = buildHtml(tdiv): + tdiv(style = style(StyleAttr.color, "red".cstring)): + text "red" + tdiv(style = style(StyleAttr.color, "blue")): + text "blue" + # explicit `kstring` required for varargs overload + tdiv(style = style((fontStyle, "italic".kstring), (color, "orange".kstring))): + text "italic orange" + # can use a string directly + tdiv(style = "font-style: oblique; color: pink".toCss): + text "oblique pink" + +setRenderer createDom diff --git a/examples/hellouniverse.nim b/examples/hellouniverse.nim new file mode 100644 index 0000000..8343d07 --- /dev/null +++ b/examples/hellouniverse.nim @@ -0,0 +1,12 @@ +include karax / prelude +import random + +proc createDom(): VNode = + result = buildHtml(tdiv): + if rand(100) <= 50: + text "Hello World!" + else: + text "Hello Universe" + +randomize() +setRenderer createDom diff --git a/examples/helloworld.nim b/examples/helloworld.nim new file mode 100644 index 0000000..76c43db --- /dev/null +++ b/examples/helloworld.nim @@ -0,0 +1,7 @@ +include karax / prelude + +proc createDom(): VNode = + result = buildHtml(tdiv): + text "Hello World!" + +setRenderer createDom diff --git a/examples/login.nim b/examples/login.nim new file mode 100644 index 0000000..0350e23 --- /dev/null +++ b/examples/login.nim @@ -0,0 +1,46 @@ +include karax / prelude +from sugar import `=>` +import karax / errors + +proc loginField(desc, field, class: kstring; + validator: proc (field: kstring): proc ()): VNode = + result = buildHtml(tdiv): + label(`for` = field): + text desc + input(class = class, id = field, onchange = validator(field)) + +# some consts in order to prevent typos: +const + username = kstring"username" + password = kstring"password" + +proc validateNotEmpty(field: kstring): proc () = + result = proc () = + let x = getVNodeById(field) + if x.text == "": + errors.setError(field, field & " must not be empty") + else: + errors.setError(field, "") + +var loggedIn: bool + +proc loginDialog(): VNode = + result = buildHtml(tdiv): + if not loggedIn: + loginField("Name: ", username, "input", validateNotEmpty) + loginField("Password: ", password, "password", validateNotEmpty) + button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()): + text "Login" + p: + text errors.getError(username) + p: + text errors.getError(password) + else: + p: + text "You are now logged in." + +setError username, username & " must not be empty" +setError password, password & " must not be empty" + +when not declared(toychat): + setRenderer loginDialog diff --git a/examples/mediaplayer/mediaplayer.nim b/examples/mediaplayer/mediaplayer.nim new file mode 100644 index 0000000..c84b68b --- /dev/null +++ b/examples/mediaplayer/mediaplayer.nim @@ -0,0 +1,42 @@ + +import karax / [karax, karaxdsl, vdom, kdom, compact] + +const + Play = 0 + Big = 1 + Small = 2 + Normal = 3 + +proc paused(n: Node): bool {.importcpp: "#.paused".} +proc play(n: Node) {.importcpp.} +proc pause(n: Node) {.importcpp.} +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.index + of Play: + if myVideo.paused: + myVideo.play() + else: + myVideo.pause() + of Big: myVideo.width = 560 + of Small: myVideo.width = 320 + of Normal: myVideo.width = 420 + else: discard + + result = buildHtml(tdiv): + button(onclick=handler, index=Play): + text "Play/Pause" + button(onclick=handler, index=Big): + text "Big" + button(onclick=handler, index=Small): + text "Small" + button(onclick=handler, index=Normal): + text "Normal" + br() + br() + video(id=id, width="420"): + source(src=resource, `type`="video/mp4"): + text "Your browser does not support HTML5 video." diff --git a/examples/mediaplayer/playerapp.html b/examples/mediaplayer/playerapp.html new file mode 100644 index 0000000..a45a2e8 --- /dev/null +++ b/examples/mediaplayer/playerapp.html @@ -0,0 +1,15 @@ + + + + + Player app + + + + +
+ + + + + diff --git a/examples/mediaplayer/playerapp.nim b/examples/mediaplayer/playerapp.nim new file mode 100644 index 0000000..acb2e68 --- /dev/null +++ b/examples/mediaplayer/playerapp.nim @@ -0,0 +1,21 @@ +## Example app that shows how to write and embed a custom component. + +include karax/prelude +import mediaplayer + +const url = "https://www.w3schools.com/html/mov_bbb.mp4" + +proc createDom(): VNode = + result = buildHtml(table): + tr: + td: + mplayer("vid1", url) + td: + mplayer("vid2", url) + tr: + td: + mplayer("vid3", url) + td: + mplayer("vid4", url) + +setRenderer createDom diff --git a/examples/mediaplayer/playerapp.nims b/examples/mediaplayer/playerapp.nims new file mode 100644 index 0000000..cf2eb9e --- /dev/null +++ b/examples/mediaplayer/playerapp.nims @@ -0,0 +1 @@ +--path: "../.." diff --git a/examples/scrollapp/scrollapp.html b/examples/scrollapp/scrollapp.html new file mode 100644 index 0000000..44d657e --- /dev/null +++ b/examples/scrollapp/scrollapp.html @@ -0,0 +1,15 @@ + + + + + Todo app + + + + +
+ + + + + diff --git a/examples/scrollapp/scrollapp.nim b/examples/scrollapp/scrollapp.nim new file mode 100644 index 0000000..4490447 --- /dev/null +++ b/examples/scrollapp/scrollapp.nim @@ -0,0 +1,25 @@ +## Example that shows how to accomplish an "infinitely scrolling" app. + +include karax/prelude +import karax / [jstrutils, kdom, vstyles] + +var entries: seq[cstring] = @[] +for i in 1..500: + entries.add(cstring("Entry ") & &i) + +proc scrollEvent(ev: Event; n: VNode) = + let d = n.dom + if d != nil and inViewport(d.lastChild): + # "load" more data: + for i in 1..50: + entries.add(cstring("Loaded Entry ") & &i) + +proc createDom(): VNode = + result = buildHtml(): + tdiv(onscroll=scrollEvent, style=style( + (StyleAttr.height, cstring"400px"), (StyleAttr.overflow, cstring"scroll"))): + for x in entries: + tdiv: + text x + +setRenderer createDom diff --git a/examples/scrollapp/scrollapp.nims b/examples/scrollapp/scrollapp.nims new file mode 100644 index 0000000..cf2eb9e --- /dev/null +++ b/examples/scrollapp/scrollapp.nims @@ -0,0 +1 @@ +--path: "../.." diff --git a/examples/todoapp/todoapp.html b/examples/todoapp/todoapp.html index 726070a..ab3daa7 100644 --- a/examples/todoapp/todoapp.html +++ b/examples/todoapp/todoapp.html @@ -1,19 +1,16 @@ - - Todo app - - + + Todo app + + - + -
+
- - + - + diff --git a/examples/todoapp/todoapp.nim b/examples/todoapp/todoapp.nim index 23840f7..e3720fd 100644 --- a/examples/todoapp/todoapp.nim +++ b/examples/todoapp/todoapp.nim @@ -1,116 +1,152 @@ -import vdom, karax, karaxdsl, jstrutils +import karax / [vdom, karax, karaxdsl, jstrutils, compact, localstorage] type Filter = enum all, active, completed var - entries: seq[(cstring, bool)] selectedEntry = -1 filter: Filter + entriesLen: int + doneswitch = true +const + contentSuffix = cstring"content" + completedSuffix = cstring"completed" + lenSuffix = cstring"entriesLen" + +proc getEntryContent(pos: int): cstring = + result = getItem(&pos & contentSuffix) + if result == cstring"null": + result = nil + +proc isCompleted(pos: int): bool = + var value = getItem(&pos & completedSuffix) + result = value == cstring"true" + +proc setEntryContent(pos: int, content: cstring) = + setItem(&pos & contentSuffix, content) + +proc markAsCompleted(pos: int, completed: bool) = + setItem(&pos & completedSuffix, &completed) + +proc addEntry(content: cstring, completed: bool) = + setEntryContent(entriesLen, content) + markAsCompleted(entriesLen, completed) + inc entriesLen + setItem(lenSuffix, &entriesLen) + +proc updateEntry(pos: int, content: cstring, completed: bool) = + setEntryContent(pos, content) + markAsCompleted(pos, completed) proc onTodoEnter(ev: Event; n: VNode) = - entries.add((n.value, false)) - n.value = "" + if n.value.strip() != "": + addEntry(n.value, false) + n.value = "" proc removeHandler(ev: Event; n: VNode) = - entries[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) = - entries[n.key][0] = n.value + setEntryContent(n.index, n.value) selectedEntry = -1 proc toggleEntry(ev: Event; n: VNode) = - let id = n.key - entries[id][1] = not entries[id][1] + let id = n.index + markAsCompleted(id, not isCompleted(id)) proc onAllDone(ev: Event; n: VNode) = - entries = @[] - selectedEntry = -1 - + for i in 0.. + + + + Karax AJAX Test + + + + +
+ + + diff --git a/experiments/ajaxtest.nim b/experiments/ajaxtest.nim new file mode 100644 index 0000000..720b6b2 --- /dev/null +++ b/experiments/ajaxtest.nim @@ -0,0 +1,6 @@ +import kdom, kajax + +proc cb(httpStatus: int, response: cstring) = + echo "Worked!" + +ajaxGet("https://httpbin.org/get", @[], cb) diff --git a/experiments/config.nims b/experiments/config.nims new file mode 100644 index 0000000..86673aa --- /dev/null +++ b/experiments/config.nims @@ -0,0 +1,2 @@ +--path: "../karax" +--path: ".." diff --git a/experiments/echartstest.html b/experiments/echartstest.html new file mode 100644 index 0000000..8c9b225 --- /dev/null +++ b/experiments/echartstest.html @@ -0,0 +1,14 @@ + + + + + Karax ECharts Test + + + + +
+ + + + diff --git a/experiments/echartstest.nim b/experiments/echartstest.nim new file mode 100644 index 0000000..950c6ce --- /dev/null +++ b/experiments/echartstest.nim @@ -0,0 +1,58 @@ +import karax / [kbase, vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, jjson] + +type + Views = enum + Customers, Products + +var + currentView = Customers + +type + EChart* = ref object + +proc echartsInit(n: Element): EChart {.importc: "echarts.init".} +proc setOption(x: EChart; option: JsonNode) {.importcpp.} + +proc postRender(data: RouterData) = + if currentView == Products: + let myChart = echartsInit(kdom.getElementById("echartSection")) + # specify chart configuration item and data + let option = %*{ + "title": { + "text": "ECharts entry example" + }, + "tooltip": {}, + "legend": { + "data": ["Sales"] + }, + "xAxis": { + "data": ["shirt","cardign","chiffon shirt","pants","heels","socks"] + }, + "yAxis": {}, + "series": [{ + "name": "Sales", + "type": "bar", + "data": [5, 20, 36, 10, 10, 20] + }] + } + myChart.setOption(option) + +proc createDom(data: RouterData): VNode = + let hash = data.hashPart + if hash == cstring"#/Products": currentView = Products + else: currentView = Customers + + result = buildHtml(tdiv): + ul(class = "tabs"): + for v in low(Views)..high(Views): + li: + a(href = "#/" & $v): + text cstring($v) + + if currentView == Products: + tdiv(id = "echartSection", style = style((width, kstring"600px"), (height, kstring"400px"))) + tdiv: + text "other section" + +setRenderer createDom, "ROOT", postRender +setForeignNodeId "echartSection" diff --git a/experiments/example.html b/experiments/example.html deleted file mode 100644 index 35b9837..0000000 --- a/experiments/example.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - Todo app - - - - - -
- - - - - - diff --git a/experiments/example.nim b/experiments/example.nim deleted file mode 100644 index 0ea805e..0000000 --- a/experiments/example.nim +++ /dev/null @@ -1,135 +0,0 @@ - -import vdom, karax, karaxdsl, jdict, jstrutils - -type - Filter = enum - all, active, completed - -var - entries: seq[(cstring, bool)] - selectedEntry = -1 - filter: Filter - -proc onTodoEnter(ev: Event; n: VNode) = - entries.add((n.value, false)) - n.value = "" - -proc removeHandler(ev: Event; n: VNode) = - let id = suffixAsInt(n.id, "remove:") - entries[id] = (cstring(nil), false) - -proc editHandler(ev: Event; n: VNode) = - let id = suffixAsInt(n.id, "edit:") - selectedEntry = id - -when defined(usecache): - var entryCache = newJDict[int, VNode]() - -proc focusLost(ev: Event; n: VNode) = selectedEntry = -1 - -proc editEntry(ev: Event; n: VNode) = - let id = suffixAsInt(n.id, "todo-edit:") - entries[id][0] = n.value - selectedEntry = -1 - -proc toggleEntry(ev: Event; n: VNode) = - let id = suffixAsInt(n.id, "toggle:") - entries[id][1] = not entries[id][1] - -proc onAllDone(ev: Event; n: VNode) = - entries = @[] - selectedEntry = -1 - -proc clearCompleted(ev: Event, n: VNode) = - for i in 0.. 0: + let p = prevSelected #selected.value + if p != nil: + if isFirstName: + p.firstname = v + else: + p.lastname = v + unselect() + errmsg <- "" + else: + errmsg <- "name must not be empty" + +var inpFirstname = toUI(true) +var inpLastname = toUI(false) + +proc adaptFocus(def = false): RBool = + result = RBool() + result.value = def + when false: + result.subscribe proc (hasFocus: bool) = + if not hasFocus: + unselect() + +var focusA = adaptFocus() +var focusB = 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 focusA.value: cstring"red" else: cstring"black")), + (StyleAttr.fontSize, cstring"12px"), + (StyleAttr.fontWeight, cstring"600") + ) + +var clicks = 0 + +proc renderUser(u: User): VNode {.track.} = + result = buildHtml(tdiv): + if u.selected: + inpFirstname <- u.firstname + tdiv: + textInput inpFirstname, focusA + inpLastname <- u.lastname + tdiv: + textInput inpLastname, focusB + else: + button: + text "..." + proc onclick(ev: Event; n: VNode) = + select(u) + text u.firstname & " " & u.lastname + button: + text "(x)" + proc onclick(ev: Event; n: VNode) = + gu.deleteElem(u) + +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(rawFirstname: "Added", rawLastname: &clicks) + tdiv: + text errmsg + vmapIt(gu, tdiv, renderUser(it)) + +proc init(): VNode = main(gu) + +setInitializer(init) diff --git a/experiments/oldwidgets.nim b/experiments/oldwidgets.nim new file mode 100644 index 0000000..c947d7f --- /dev/null +++ b/experiments/oldwidgets.nim @@ -0,0 +1,251 @@ +var + linkCounter: int + +proc link*(id: int): VNode = + result = newVNode(VNodeKind.anchor) + result.setAttr("href", "#") + inc linkCounter + result.setAttr("id", $linkCounter & ":" & $id) + +proc link*(action: EventHandler): VNode = + result = newVNode(VNodeKind.anchor) + result.setAttr("href", "#") + result.setOnclick action + +when false: + proc button*(caption: cstring; action: EventHandler; disabled=false): VNode = + result = newVNode(VNodeKind.button) + result.add text(caption) + if action != nil: + result.setOnClick action + if disabled: + result.setAttr("disabled", "true") + +proc select*(choices: openarray[cstring]): VNode = + result = newVNode(VNodeKind.select) + var i = 0 + for c in choices: + result.add tree(VNodeKind.option, [(cstring"value", toCstr(i))], text(c)) + inc i + +proc select*(choices: openarray[(int, cstring)]): VNode = + result = newVNode(VNodeKind.select) + for c in choices: + result.add tree(VNodeKind.option, [(cstring"value", toCstr(c[0]))], text(c[1])) + +var radioCounter: int + +proc radio*(choices: openarray[(int, cstring)]): VNode = + result = newVNode(VNodeKind.fieldset) + var i = 0 + inc radioCounter + for c in choices: + let id = cstring"radio_" & c[1] & toCstr(i) + var kid = tree(VNodeKind.input, [(cstring"type", cstring"radio"), + (cstring"id", id), (cstring"name", cstring"radio" & toCStr(radioCounter)), + (cstring"value", toCStr(c[0]))]) + if i == 0: + kid.setAttr(cstring"checked", cstring"checked") + var lab = tree(VNodeKind.label, [(cstring"for", id)], text(c[1])) + kid.add lab + result.add kid + inc i + +proc tag*(kind: VNodeKind; id=cstring(nil), class=cstring(nil)): VNode = + result = newVNode(kind) + result.id = id + result.class = class + +proc tdiv*(id=cstring(nil), class=cstring(nil)): VNode = tag(VNodeKind.tdiv, id, class) +proc span*(id=cstring(nil), class=cstring(nil)): VNode = tag(VNodeKind.span, id, class) + +proc valueAsInt*(e: Node): int = parseInt(e.value) + +proc th*(s: cstring): VNode = + result = newVNode(VNodeKind.th) + result.add text(s) + +proc td*(s: string): VNode = + result = newVNode(VNodeKind.td) + result.add text(s) + +proc td*(s: VNode): VNode = + result = newVNode(VNodeKind.td) + result.add s + +proc td*(class: cstring; s: VNode): VNode = + result = newVNode(VNodeKind.td) + result.add s + result.class = class + +proc table*(class=cstring(nil), kids: varargs[VNode]): VNode = + result = tag(VNodeKind.table, nil, class) + for k in kids: result.add k + +proc tr*(kids: varargs[VNode]): VNode = + result = newVNode(VNodeKind.tr) + for k in kids: + if k.kind in {VNodeKind.td, VNodeKind.th}: + result.add k + else: + result.add td(k) + +proc suffix*(s, prefix: cstring): cstring = + if s.startsWith(prefix): + result = s.substr(prefix.len) + else: + kout(cstring"bug! " & s & cstring" does not start with " & prefix) + +proc suffixAsInt*(s, prefix: cstring): int = parseInt(suffix(s, prefix)) + +#proc ceil(f: float): int {.importc: "Math.ceil", nodecl.} + +proc realtimeInput*(val: cstring; action: EventHandler): VNode = + var timer: Timeout + proc onkeyup(ev: Event; n: VNode) = + proc wrapper() = keyeventBody() + + if timer != nil: clearTimeout(timer) + timer = setTimeout(wrapper, 400) + result = tree(VNodeKind.input, [(cstring"type", cstring"text")]) + result.value = val + result.addEventListener(EventKind.onkeyup, onkeyup) + +proc enterInput*(id, val: cstring; action: EventHandler): VNode = + proc onkeyup(ev: Event; n: VNode) = + if ev.keyCode == 13: keyeventBody() + + result = tree(VNodeKind.input, [(cstring"type", cstring"text")]) + result.id = id + result.value = val + result.addEventListener(EventKind.onkeyup, onkeyup) + +proc setOnEnter*(n: VNode; action: EventHandler) = + proc onkeyup(ev: Event; n: VNode) = + if ev.keyCode == 13: keyeventBody() + n.addEventListener(EventKind.onkeyup, onkeyup) + +proc setOnscroll*(action: proc(min, max: VKey; diff: int)) = + var oldY = window.pageYOffset + + proc wrapper(ev: Event) = + let dir = window.pageYOffset - oldY + if dir == 0: return + + var a = VKey high(int) + var b = VKey 0 + var h, count: int + document.visibleKeys(a, b, h, count) + let avgh = h / count + let diff = toInt(dir.float / avgh) + if diff != 0: + oldY = window.pageYOffset + action(a, b, diff) + redraw() + + document.addEventListener("scroll", wrapper) + +when false: + var plugins {.exportc.}: seq[(string, proc())] = @[] + + proc onInput(val: cstring) = + kout val + if val == "dyn": + kout(plugins.len) + if plugins.len > 0: + plugins[0][1]() + +var + images: seq[cstring] = @[cstring"a", "b", "c", "d"] + +proc carousel*(): VNode = + var currentIndex = 0 + + proc next(ev: Event; n: VNode) = + currentIndex = (currentIndex + 1) mod images.len + + proc prev(ev: Event; n: VNode) = + currentIndex = (currentIndex - 1) mod images.len + + result = buildHtml(tdiv): + text images[currentIndex] + button(onclick = next): + text "Next" + button(onclick = prev): + text "Previous" + +#proc targetElem*(e: Event): Element = cast[Element](e.target) + +#proc getElementsByClassName*(cls: cstring): seq[Element] {.importc: +# "document.getElementsByClassName", nodecl.} +#proc textContent(e: Node): cstring {. +# importcpp: "#.textContent", nodecl.} + +proc isElementInViewport(el: Node; h: var int): bool = + let rect = el.getBoundingClientRect() + h = rect.bottom - rect.top + result = rect.top >= 0 and rect.left >= 0 and + rect.bottom <= clientHeight() and + rect.right <= clientWidth() + +proc visibleKeys(e: Node; a, b: var VKey; h, count: var int) = + # we only care about nodes that have a key: + var hh = 0 + # do not recurse if there is a 'key' field already: + if e.key >= 0: + if isElementInViewport(e, hh): + inc count + inc h, hh + a = min(a, e.key) + b = max(b, e.key) + else: + for i in 0.. + + + + + Refs + + + +
+ + + diff --git a/experiments/refs/demo.nim b/experiments/refs/demo.nim new file mode 100644 index 0000000..a0db59f --- /dev/null +++ b/experiments/refs/demo.nim @@ -0,0 +1,58 @@ +include karaxprelude +import future, sequtils + +type + CustomRef = ref object of VComponent + +method onAttach(r: CustomRef) = + kout(cstring"custom ref attached") + +method onDetach(r: CustomRef) = + kout(cstring"custom ref detached") + +var + modelData = @[5, 2, 4] + + refA = VComponent() + refB = VComponent() + refC = CustomRef() + refSeq = newSeq[VComponent]() + +proc onClick(ev: Event, n: VNode) = + modelData.add(0) + +proc showRefs() = + kout(refA) + kout(refB) + kout(refC) + for i in 0 ..< refSeq.len: + kout(refSeq[i].vnode) + #kout(refSeq.map(nref => nref.vnode)) + +proc secureRefSlot(i: int): VComponent = + while refSeq.len <= i: + refSeq.add(VComponent()) + result = refSeq[i] + +proc view(): VNode = + result = buildHtml(): + tdiv: + button(onclick=onClick): + text "click me" + # storing refs to single elements is straightforward now + 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)): + text "List item: " & $x + +proc renderer(): VNode = + view() + +setRenderer(renderer, "ROOT", showRefs) 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/experiments/scrollapp.html b/experiments/scrollapp.html index ad3ce2f..8b909f1 100644 --- a/experiments/scrollapp.html +++ b/experiments/scrollapp.html @@ -10,7 +10,7 @@
- + diff --git a/experiments/scrollapp.nim b/experiments/scrollapp.nim index 7b7ce8e..86a802f 100644 --- a/experiments/scrollapp.nim +++ b/experiments/scrollapp.nim @@ -1,17 +1,14 @@ -import vdom, karax, karaxdsl, jdict, jstrutils +import vdom, karax, karaxdsl, jdict, jstrutils, compact type Filter = enum all, active, completed -const - WindowSize = 50 # overestimating it does not harm - var entries: seq[(cstring, bool)] visibleA = 0 - visibleB = 10_000 + visibleB = 10 selectedEntry = -1 filter: Filter @@ -52,34 +49,15 @@ proc toChecked(checked: bool): cstring = proc selected(v: Filter): cstring = (if filter == v: cstring"selected" else: cstring(nil)) -when false: - proc entry(id: int, d: cstring; completed, selected: bool): proc (): VNode = - var lastid: type(id) - var lastd: type(d) - var lastCompleted: type(completed) - var lastSelected: type(selected) - proc inner(): VNode = - if lastid != id or lastd != d or lastCompleted != completed or lastSelected != selected: - result = logic(id, d, completed, selected) - lastid = id - lastd = d - lastCompleted = completed - lastSelected = selected - - result = inner - -proc lazyEntry(args: seq[VNode]): VNode = - let id = args[0].intValue - let d = args[1].text - let completed = args[2].intValue != 0 - let selected = args[3].intValue != 0 +proc createEntry(id: int; d: cstring; + completed: bool, selected: bool): VNode {.component.} = result = buildHtml(tr): li(class=toClass(completed), key = id): if not selected: tdiv(class = "view"): input(class = "toggle", `type` = "checkbox", checked = toChecked(completed), onclick=toggleEntry, key = id) - label(onDblClick=editHandler, key = id): + label(onclick=editHandler, key = id): text d button(class = "destroy", key = id, onclick = removeHandler) else: @@ -87,9 +65,6 @@ proc lazyEntry(args: seq[VNode]): VNode = onfocusLost = focusLost, onenter = editEntry, value = d, setFocus) -proc createEntry(id: int; d: cstring; completed, selected: bool): VNode = - result = thunk(lazyEntry, id, d, completed, selected) - proc myscroll(ev: Event; n: VNode) = kout cstring"scrolling event here" @@ -105,10 +80,6 @@ proc createDom(): VNode = input(class = "toggle-all", `type` = "checkbox", name = "toggle") label(`for` = "toggle-all", onclick = onAllDone): text "Mark all as complete" - tdiv(onscroll = myscroll, style = "overflow: visible; width: 0px; display: block; max-height: 1em"): - for i in 0..4: - tdiv: - text entries[i][0] var entriesCount = 0 var completedCount = 0 diff --git a/experiments/scrollapp_simple.nim b/experiments/scrollapp_simple.nim new file mode 100644 index 0000000..cdd19cd --- /dev/null +++ b/experiments/scrollapp_simple.nim @@ -0,0 +1,25 @@ +## Example that shows how to accomplish an "infinitely scrolling" app. + +include karax / prelude +import karax / [kdom, vstyles] + +var entries: seq[cstring] = @[] +for i in 1..500: + entries.add(cstring("Entry ") & &i) + +proc scrollEvent(ev: Event; n: VNode) = + let d = n.dom + if d != nil and inViewport(d.lastChild): + # "load" more data: + for i in 1..50: + entries.add(cstring("Loaded Entry ") & &i) + +proc createDom(): VNode = + result = buildHtml(): + tdiv(onscroll=scrollEvent, style=style( + (StyleAttr.height, cstring"400px"), (StyleAttr.overflow, cstring"scroll"))): + for x in entries: + tdiv: + text x + +setRenderer createDom diff --git a/experiments/style.css b/experiments/style.css new file mode 100644 index 0000000..ceaf090 --- /dev/null +++ b/experiments/style.css @@ -0,0 +1,378 @@ +html, +body { + margin: 0; + padding: 0; +} + +.todomvc-wrapper { + visibility: visible !important; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #4d4d4d; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; + font-weight: 300; +} + +button, +input[type="checkbox"] { + outline: none; +} + +.hidden { + display: none; +} + +.todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp h1 { + position: absolute; + top: -155px; + width: 100%; + font-size: 100px; + font-weight: 100; + text-align: center; + color: rgba(175, 47, 47, 0.15); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + border: 0; + outline: none; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +.new-todo { + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +label[for='toggle-all'] { + display: none; +} + +.toggle-all { + position: absolute; + top: -55px; + left: -12px; + width: 60px; + height: 34px; + text-align: center; + border: none; /* Mobile Safari */ +} + +.toggle-all:before { + content: '>'; + font-size: 22px; + color: #e6e6e6; + padding: 10px 27px 10px 27px; +} + +.toggle-all:checked:before { + color: #737373; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +.todo-list li:last-child { + border-bottom: none; +} + +.todo-list li.editing { + border-bottom: none; + padding: 0; +} + +.todo-list li.editing .edit { + display: block; + width: 506px; + padding: 13px 17px 12px 17px; + margin: 0 0 0 43px; +} + +.todo-list li.editing .view { + display: none; +} + +.todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +.todo-list li .toggle:after { + content: url('data:image/svg+xml,'); +} + +.todo-list li .toggle:checked:after { + content: url('data:image/svg+xml,'); +} + +.todo-list li label { + white-space: pre-line; + word-break: break-all; + padding: 15px 60px 15px 15px; + margin-left: 45px; + display: block; + line-height: 1.2; + transition: color 0.4s; +} + +.todo-list li.completed label { + color: #d9d9d9; + text-decoration: line-through; +} + +.todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #cc9a9a; + margin-bottom: 11px; + transition: color 0.2s ease-out; +} + +.todo-list li .destroy:hover { + color: #af5b5e; +} + +.todo-list li .destroy:after { + content: 'x'; +} + +.todo-list li:hover .destroy { + display: block; +} + +.todo-list li.editing:last-child { + margin-bottom: -1px; +} + +.footer { + color: #777; + padding: 10px 15px; + height: 20px; + text-align: center; + border-top: 1px solid #e6e6e6; +} + +.footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + float: left; + text-align: left; +} + +.todo-count strong { + font-weight: 300; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a.selected, +.filters li a:hover { + border-color: rgba(175, 47, 47, 0.1); +} + +.filters li a.selected { + border-color: rgba(175, 47, 47, 0.2); +} + +.clear-completed, +html .clear-completed:active { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + cursor: pointer; + position: relative; +} + +.clear-completed:hover { + text-decoration: underline; +} + +.info { + margin: 65px auto 0; + color: #bfbfbf; + /* font-size: 10px; */ + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.info p { + line-height: 1; +} + +.info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.info a:hover { + text-decoration: underline; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox +*/ +@media screen and (-webkit-min-device-pixel-ratio:0) { + .toggle-all, + .todo-list li .toggle { + background: none; + } + + .todo-list li .toggle { + height: 40px; + } + + .toggle-all { + -webkit-transform: rotate(90deg); + transform: rotate(90deg); + -webkit-appearance: none; + appearance: none; + } +} + +@media (max-width: 430px) { + .footer { + height: 50px; + } + + .filters { + bottom: 10px; + } +} \ No newline at end of file diff --git a/experiments/trello/knete.nim b/experiments/trello/knete.nim new file mode 100644 index 0000000..3a09ee3 --- /dev/null +++ b/experiments/trello/knete.nim @@ -0,0 +1,752 @@ +## Raw DOM manipulation. No DOM diff'ing, no cry. + +import macros, tables +from strutils import startsWith, toLowerAscii, toUpperAscii + +when defined(js): + type kstring* = cstring +else: + type kstring* = string + +type + Tag* {.pure.} = enum + text = "#text", + + html, head, title, base, link, meta, style, + script, noscript, + body, section, nav, article, aside, + h1, h2, h3, h4, h5, h6, + header, footer, address, main + + p, hr, pre, blockquote, ol, ul, li, + dl, dt, dd, + figure, figcaption, + + tdiv = "div", + + a, em, strong, small, + strikethrough = "s", cite, quote, + dfn, abbr, data, time, code, `var` = "var", samp, + kdb, sub, sup, italic = "i", bold = "b", underlined = "u", + mark, ruby, rt, rp, bdi, dbo, span, br, wbr, + ins, del, img, iframe, embed, `object` = "object", + param, video, audio, source, track, canvas, map, + area, svg, math, + + table, caption, colgroup, col, tbody, thead, + tfoot, tr, td, th, + + form, fieldset, legend, label, input, button, + select, datalist, optgroup, option, textarea, + keygen, output, progress, meter, + details, summary, command, menu + +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. + 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. + + onmousedown, ## A pointing device button (usually a mouse) is pressed + ## on an element. + onmouseenter, ## A pointing device is moved onto the element that + ## has the listener attached. + onmouseleave, ## A pointing device is moved off the element that + ## has the listener attached. + onmousemove, ## A pointing device is moved over an element. + onmouseout, ## A pointing device is moved off the element that + ## has the listener attached or off one of its children. + onmouseover, ## A pointing device is moved onto the element that has + ## the listener attached or onto one of its children. + onmouseup, ## A pointing device button is released over an element. + + ondrag, ## An element or text selection is being dragged (every 350ms). + ondragend, ## A drag operation is being ended (by releasing a mouse button + ## or hitting the escape key). + ondragenter, ## A dragged element or text selection enters a valid drop target. + ondragleave, ## A dragged element or text selection leaves a valid drop target. + ondragover, ## An element or text selection is being dragged over a valid + ## drop target (every 350ms). + 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) + + +macro buildLookupTables(): untyped = + var a = newTree(nnkBracket) + for i in low(Tag)..high(Tag): + let x = $i + let y = if x[0] == '#': x else: toUpperAscii(x) + a.add(newCall("kstring", newLit(y))) + var e = newTree(nnkBracket) + for i in low(EventKind)..high(EventKind): + e.add(newCall("kstring", newLit(substr($i, 2)))) + + template tmpl(a, e) {.dirty.} = + const + toTagName*: array[Tag, kstring] = a + toEventName*: array[EventKind, kstring] = e + + result = getAst tmpl(a, e) + +buildLookupTables() + +# ---------------- CSS ---------------------------------------------- + +type + 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 + listStyle + listStyleImage + listStylePosition + listStyleType + margin + marginBottom + marginLeft + marginRight + marginTop + maxHeight + maxWidth + minHeight + minWidth + navDown + navIndex + navLeft + navRight + navUp + opacity + order + orphans + outline + outlineColor + outlineOffset + outlineStyle + outlineWidth + overflow + overflowX + overflowY + padding + paddingBottom + paddingLeft + paddingRight + paddingTop + pageBreakAfter + pageBreakBefore + pageBreakInside + perspective + perspectiveOrigin + pointerEvents + position + quotes + resize + right + 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 buildStyleLookupTable(): untyped = + var e = newTree(nnkBracket) + for i in low(StyleAttr)..high(StyleAttr): + e.add(newCall("kstring", newLit($i))) + template tmpl(e) {.dirty.} = + const + toStyleAttrName: array[StyleAttr, kstring] = e + result = getAst tmpl(e) + +buildStyleLookupTable() + +type + StyleObj {.importc.} = object + background*: cstring + backgroundAttachment*: cstring + backgroundColor*: cstring + backgroundImage*: cstring + backgroundPosition*: cstring + backgroundRepeat*: cstring + border*: cstring + borderBottom*: cstring + borderBottomColor*: cstring + borderBottomStyle*: cstring + borderBottomWidth*: cstring + borderColor*: cstring + borderLeft*: cstring + borderLeftColor*: cstring + borderLeftStyle*: cstring + borderLeftWidth*: cstring + borderRight*: cstring + borderRightColor*: cstring + borderRightStyle*: cstring + borderRightWidth*: cstring + borderStyle*: cstring + borderTop*: cstring + borderTopColor*: cstring + borderTopStyle*: cstring + borderTopWidth*: cstring + borderWidth*: cstring + bottom*: cstring + captionSide*: cstring + clear*: cstring + clip*: cstring + color*: cstring + cursor*: cstring + direction*: cstring + display*: cstring + emptyCells*: cstring + cssFloat*: cstring + font*: cstring + fontFamily*: cstring + fontSize*: cstring + fontStretch*: cstring + fontStyle*: cstring + fontVariant*: cstring + fontWeight*: cstring + height*: cstring + left*: cstring + letterSpacing*: cstring + lineHeight*: cstring + listStyle*: cstring + listStyleImage*: cstring + listStylePosition*: cstring + listStyleType*: cstring + margin*: cstring + marginBottom*: cstring + marginLeft*: cstring + marginRight*: cstring + marginTop*: cstring + maxHeight*: cstring + maxWidth*: cstring + minHeight*: cstring + minWidth*: cstring + overflow*: cstring + padding*: cstring + paddingBottom*: cstring + paddingLeft*: cstring + paddingRight*: cstring + paddingTop*: cstring + pageBreakAfter*: cstring + pageBreakBefore*: cstring + pointerEvents*: cstring + position*: cstring + right*: cstring + scrollbar3dLightColor*: cstring + scrollbarArrowColor*: cstring + scrollbarBaseColor*: cstring + scrollbarDarkshadowColor*: cstring + scrollbarFaceColor*: cstring + scrollbarHighlightColor*: cstring + scrollbarShadowColor*: cstring + scrollbarTrackColor*: cstring + tableLayout*: cstring + textAlign*: cstring + textDecoration*: cstring + textIndent*: cstring + textTransform*: cstring + transform*: cstring + top*: cstring + verticalAlign*: cstring + visibility*: cstring + width*: cstring + wordSpacing*: cstring + zIndex*: int + Style* = ref StyleObj + + ElementObj {.importc.} = object + id*, class*, value*: cstring + disabled*: bool + style*: Style + Element* = ref ElementObj + + DocumentObj {.importc.} = object + Document = ref DocumentObj + +proc setStyle(s: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.} + +proc applyStyles*(e: Element; pairs: openArray[(StyleAttr, kstring)]) = + for x in pairs: + e.style.setStyle(toStyleAttrName[x[0]], x[1]) + +# ------- Tree manipulation --------------------------- + +var document {.importc.}: Document + +proc parent*(x: Element): Element {.importcpp: "#.parentNode".} + +proc up*(x: Element; className: cstring): Element = + result = x + while result != nil and result.class != className: + result = result.parent + +proc len*(x: Element): int {.importcpp: "#.childNodes.length".} +proc `[]`*(x: Element; idx: int): Element {.importcpp: "#.childNodes[#]".} +proc getElementById*(id: cstring): Element {.importc: "document.getElementById", nodecl.} +proc add*(n, child: Element) {.importcpp: "appendChild".} + +proc removeChild(n, child: Element) {.importcpp.} +proc replaceChild(n, newNode, oldNode: Element) {.importcpp.} + +proc replace*(self, by: Element) = replaceChild(self.parent, by, self) +proc delete*(self: Element) = removeChild(self.parent, self) + +proc insertBefore(n, newNode, before: Element) {.importcpp.} +proc insert*(before, newNode: Element) = insertBefore(before.parent, newNode, before) + +proc createTextNode(d: Document, text: cstring): Element {.importcpp.} +proc text*(s: kstring): Element = createTextNode(document, s) + +iterator items*(n: Element): Element = + for i in 0.. 0: + result[L-1] = tcall2(result[L-1], tmpContext) + 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 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 Element to attach the event handler to" + else: + result = newCall(evHandler(), tmpContext, + newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi")) + else: + result = n + of nnkVarSection, nnkLetSection, nnkConstSection: + result = n + of nnkCallKinds - {nnkInfix}: + let op = getName(n[0]) + if isComponent(op) == ComponentKind.Tag: + let tmp = genSym(nskLet, "tmp") + let call = newCall(bindSym"tree", newDotExpr(bindSym"Tag", n[0])) + result = newTree( + if tmpContext == nil: nnkStmtListExpr else: nnkStmtList, + newLetStmt(tmp, call)) + for i in 1 ..< n.len: + # named parameters are transformed into attributes or events: + let x = n[i] + if x.kind == nnkExprEqExpr: + let key = getName x[0] + if key.startsWith("on"): + result.add newCall(evHandler(), + tmp, newDotExpr(bindSym"EventKind", x[0]), x[1]) + elif key in SpecialAttrs: + result.add newDotAsgn(tmp, key, x[1]) + elif eqIdent(key, "style"): + result.add newCall(bindSym"applyStyles", tmp, toKstring x[1]) + elif eqIdent(key, "setFocus"): + result.add newCall(bindSym"focus", tmp) + else: + result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1]) + elif eqIdent(x, "setFocus"): + result.add newCall(bindSym"focus", tmp) + else: + result.add tcall2(x, tmp) + if tmpContext == nil: + result.add tmp + else: + result.add newCall(bindSym"add", tmpContext, tmp) + elif tmpContext != nil and op notin StmtContext: + var hasEventHandlers = false + for i in 1..= 0.16.1" +requires "nim >= 0.18.0" +requires "ws" +requires "dotenv" +skipDirs = @["examples", "experiments", "tests"] -skipDirs = @["examples/todoapp"] \ No newline at end of file +bin = @["karax/tools/karun"] +installExt = @["nim"] diff --git a/karax/autocomplete.nim b/karax/autocomplete.nim new file mode 100644 index 0000000..210ed4a --- /dev/null +++ b/karax/autocomplete.nim @@ -0,0 +1,160 @@ + +import karax, karaxdsl, vdom, kdom, jstrutils + +type + AutocompleteState* = ref object + choices, candidates: seq[cstring] + selected, maxMatches: int + showCandidates, controlPressed: bool + +proc newAutocomplete*(choices: seq[cstring]; maxMatches = 5): AutocompleteState = + ## Creates a new state for the autocomplete widget. ``maxMatches`` is the maximum + ## number of elements to show. + AutocompleteState(choices: choices, candidates: @[], + selected: -1, maxMatches: maxMatches, showCandidates: false, + controlPressed: false) + +proc autocomplete*(s: AutocompleteState; onselection: proc(s: cstring)): VNode = + var inp: VNode + + proc commit(ev: Event) = + s.showCandidates = false + onselection(inp.dom.value) + when false: + if inp.dom != nil: + echo "setting to A ", inp.dom.value.isNil + result.text = inp.dom.value + else: + echo "setting to B ", inp.text.isNil + result.text = inp.text + for e in result.events: + if e[0] == EventKind.onchange: + e[1](ev, result) + + proc onkeyuplater(ev: Event; n: VNode) = + if not s.controlPressed: + let v = n.value + s.candidates.setLen 0 + for c in s.choices: + if v.len == 0 or c.containsIgnoreCase(v): s.candidates.add(c) + + proc onkeydown(ev: Event; n: VNode) = + const + LEFT = 37 + UP = 38 + RIGHT = 39 + DOWN = 40 + TAB = 9 + ESC = 27 + ENTER = 13 + # UP: Move focus to the previous item. If on first item, move focus to the input. + # If on the input, move focus to last item. + # DOWN: Move focus to the next item. If on last item, move focus to the input. + # If on the input, move focus to the first item. + # ESCAPE: Close the menu. + # ENTER: Select the currently focused item and close the menu. + # TAB: Select the currently focused item, close the menu, and + # move focus to the next focusable element + s.controlPressed = false + case ev.keyCode + of UP: + s.controlPressed = true + s.showCandidates = true + if s.selected > 0: + dec s.selected + n.setInputText s.candidates[s.selected] + of DOWN: + s.controlPressed = true + s.showCandidates = true + if s.selected < s.candidates.len - 1: + inc s.selected + n.setInputText s.candidates[s.selected] + of ESC: + s.showCandidates = false + s.controlPressed = true + of ENTER: + s.controlPressed = true +# inp.setInputText s.choices[i] + commit(ev) + else: + discard + + proc window(s: AutocompleteState): (int, int) = + var first, last: int + if s.selected >= 0: + first = s.selected - (s.maxMatches div 2) + last = s.selected + (s.maxMatches div 2) + if first < 0: first = 0 + # too few because we had to trim first? + if (last - first + 1) < s.maxMatches: last = first + s.maxMatches - 1 + else: + first = 0 + last = s.maxMatches - 1 + if last > high(s.candidates): last = high(s.candidates) + # still too few because we're at the end? + if (last - first + 1) < s.maxMatches: + first = last - s.maxMatches + 1 + if first < 0: first = 0 + + result = (first, last) + + inp = buildHtml: + input(onkeyuplater = onkeyuplater, + onkeydown = onkeydown, + # onblur = proc (ev: Event; n: VNode) = commit(ev), + onfocus = proc (ev: Event; n: VNode) = + onkeyuplater(ev, n) + s.showCandidates = true) + + proc select(i: int): proc(ev: Event; n: VNode) = + result = proc(ev: Event; n: VNode) = + s.selected = i + s.showCandidates = false + inp.setInputText s.choices[i] + commit(ev) + + result = buildHtml(table): + tr: + td: + inp + if s.showCandidates: + let (first, last) = window(s) + for i in first..last: + tr: + td(onclick = select(i), class = cstring"button " & + (if i == s.selected: cstring"is-primary" else: "")): + text s.candidates[i] + +when isMainModule: + const suggestions = @[cstring"ActionScript", + "AppleScript", + "Asp", + "BASIC", + "C", + "C++", + "Clojure", + "COBOL", + "Erlang", + "Fortran", + "Groovy", + "Haskell", + "Java", + "JavaScript", + "Lisp", + "Nim", + "Perl", + "PHP", + "Python", + "Ruby", + "Scala", + "Scheme"] + + var s = newAutocomplete(suggestions) + + proc main(): VNode = + result = buildHtml(tdiv): + autocomplete(s, proc (s: cstring) = echo "now ", s)#: + # proc onchange(ev: Event; n: VNode) = + # echo "now selected ", n.kind + + setRenderer(main) diff --git a/karax/compact.nim b/karax/compact.nim new file mode 100644 index 0000000..4e5dd6d --- /dev/null +++ b/karax/compact.nim @@ -0,0 +1,114 @@ +## Components in Karax are built by the ``.component`` macro annotation. + +when defined(js): + import jdict, kdom + +import macros, vdom, tables, strutils, kbase + +when defined(js): + var + vcomponents* = newJDict[cstring, proc(args: seq[VNode]): VNode]() +else: + var + vcomponents* = newTable[kstring, proc(args: seq[VNode]): VNode]() + +type + ComponentKind* {.pure.} = enum + None, + Tag, + VNode + +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.strVal) + else: + result = n + +when defined(js): + macro compact*(prc: untyped): untyped = + ## A 'compact' tree generation proc is one that only depends on its + ## inputs and should be stored as a compact virtual DOM tree and + ## only expanded on demand (when its inputs changed). + var n = prc.copyNimNode + for i in 0..6: n.add prc[i].copyNimTree + expectKind(n, nnkProcDef) + if n[0].kind == nnkEmpty: + error("please pass a non anonymous proc", n[0]) + let name = n[0] + let params = params(n) + let rettype = repr params[0] + var isvirtual = ComponentKind.None + if rettype == "VNode": + isvirtual = ComponentKind.VNode + else: + error "component must return VNode", params[0] + let realName = if name.kind == nnkPostfix: name[1] else: name + let nn = $realName + n[0] = ident("inner" & nn) + var unpackCall = newCall(n[0]) + var counter = 0 + for i in 1 ..< params.len: + let param = params[i] + let L = param.len + let typ = param[L-2] + for j in 0 .. L-3: + unpackCall.add unpack(typ, counter) + inc counter + + template vwrapper(pname, unpackCall) {.dirty.} = + proc pname(args: seq[VNode]): VNode = + unpackCall + + template vregister(key, val) = + bind jdict.`[]=` + `[]=`(vcomponents, kstring(key), val) + + result = newTree(nnkStmtList, n) + + if isvirtual == ComponentKind.VNode: + result.add getAst(vwrapper(newname name, unpackCall)) + result.add getAst(vregister(newLit(nn), realName)) + allcomponents[nn] = isvirtual + when defined(debugKaraxDsl): + echo repr result diff --git a/karax/errors.nim b/karax/errors.nim new file mode 100644 index 0000000..aedd103 --- /dev/null +++ b/karax/errors.nim @@ -0,0 +1,32 @@ +## Error handling logic for form input validation. + +import karax, jdict + +var + gerrorMsgs = newJDict[cstring, cstring]() + gerrorCounter = 0 + +proc hasErrors*(): bool = gerrorCounter != 0 + +proc hasError*(field: cstring): bool = gerrorMsgs.contains(field) and len(gerrorMsgs[field]) > 0 + +proc disableOnError*(): cstring = toDisabled(hasErrors()) + +proc getError*(field: cstring): cstring = + if not gerrorMsgs.contains(field): + result = "" + else: + result = gerrorMsgs[field] + +proc setError*(field, msg: cstring) = + let previous = getError(field) + if len(msg) == 0: + if len(previous) != 0: dec(gerrorCounter) + else: + if len(previous) == 0: inc(gerrorCounter) + gerrorMsgs[field] = msg + +proc clearErrors*() = + let m = gerrorMsgs + for k in m.keys: + setError(k, "") diff --git a/karax/i18n.nim b/karax/i18n.nim new file mode 100644 index 0000000..42274ff --- /dev/null +++ b/karax/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/karax/jdict.nim b/karax/jdict.nim new file mode 100644 index 0000000..c9e2224 --- /dev/null +++ b/karax/jdict.nim @@ -0,0 +1,30 @@ + +type + JDict*[K, V] = ref object + +proc `[]`*[K, V](d: JDict[K, V], k: K): V {.importcpp: "#[#]".} +proc `[]=`*[K, V](d: JDict[K, V], k: K, v: V) {.importcpp: "#[#] = #".} + +proc newJDict*[K, V](): JDict[K, V] {.importcpp: "{@}".} + +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 #[#]".} + +iterator keys*[K, V](d: JDict[K, V]): K = + var kkk: K + {.emit: ["for (", kkk, " in ", d, ") {"].} + yield kkk + {.emit: ["}"].} + +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](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.} + +proc shrink*[T](s: JSeq[T]; shorterLen: int) {.importcpp: "#.length = #", noSideEffect.} diff --git a/karax/jjson.nim b/karax/jjson.nim new file mode 100644 index 0000000..ad6106b --- /dev/null +++ b/karax/jjson.nim @@ -0,0 +1,67 @@ +## This module implements some small zero-overhead 'JsonNode' type +## and helper that maps directly to JavaScript objects. + +type + JsonNode* {.importc.} = ref object + +proc `[]`*(obj: JsonNode; fieldname: cstring): JsonNode {.importcpp: "#[#]".} +proc `[]`*(obj: JsonNode; index: int): JsonNode {.importcpp: "#[#]".} +proc `[]=`*[T](obj: JsonNode; fieldname: cstring; value: T) + {.importcpp: "#[#] = #".} +proc length(x: JsonNode): int {.importcpp: "#.length".} +proc len*(x: JsonNode): int = (if x.isNil: 0 else: x.length) + +proc newJsonNode*(fields: varargs[(cstring, JsonNode)]): JsonNode = + result = JsonNode() + for f in fields: + result[f[0]] = f[1] + +proc newJObject*(): JsonNode = + result = JsonNode() + +proc newJArray*(elements: varargs[JsonNode]): JsonNode {.importcpp: "#".} + +proc newJNull*(): JsonNode = nil + +template `%`*(x: typed): JsonNode = cast[JsonNode](x) +template `%`*(x: string): JsonNode = cast[JsonNode](cstring x) + +proc getNum*(x: JsonNode): int {.importcpp: "#".} + +proc getInt*(x: JsonNode): int {.importcpp: "#".} +proc getStr*(x: JsonNode): cstring {.importcpp: "#".} +proc getFNum*(x: JsonNode): cstring {.importcpp: "#".} + +iterator items*(x: JsonNode): JsonNode = + for i in 0..=0)", nodecl.} + +proc containsIgnoreCase*(a, b: cstring): bool {. + importcpp: """(#.search(new RegExp(#.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$$\|]/g, "\\$$&") , "i"))>=0)""", nodecl.} + proc substr*(s: cstring; start: int): cstring {.importcpp: "substr", nodecl.} proc substr*(s: cstring; start, length: int): cstring {.importcpp: "substr", nodecl.} @@ -14,10 +20,14 @@ 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.} 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.} +proc parseFloat*(s: cstring): BiggestFloat {.importc, nodecl.} diff --git a/karax/jwebsockets.nim b/karax/jwebsockets.nim new file mode 100644 index 0000000..7c94da7 --- /dev/null +++ b/karax/jwebsockets.nim @@ -0,0 +1,17 @@ +## Websockets support for the JSON backend. + +type + MessageEvent* {.importc.} = ref object + data*: cstring + + WebSocket* {.importc.} = ref object + onmessage*: proc (e: MessageEvent) + onopen*: proc (e: MessageEvent) + +proc newWebSocket*(url, key: cstring): WebSocket + {.importcpp: "new WebSocket(@)".} + +proc newWebSocket*(url: cstring): WebSocket + {.importcpp: "new WebSocket(@)".} + +proc send*(w: WebSocket; data: cstring) {.importcpp.} diff --git a/karax/kajax.nim b/karax/kajax.nim new file mode 100644 index 0000000..4235b50 --- /dev/null +++ b/karax/kajax.nim @@ -0,0 +1,132 @@ +## Karax -- Single page applications for Nim. + +## This module implements support for `ajax`:idx: socket +## handling. + +import karax +import jsffi except `&` +import jscore +import dom + +type + ProgressEvent* {.importc.}= object + loaded*: float + total*: float + + FormData* {.importc.} = JsObject + + HttpRequest* {.importc.} = ref object + ThisObj {.importc.} = ref object + readyState, status: int + responseText, statusText: cstring + + XMLHttpRequestUpload* {.importc.} = JsObject + +proc newFormData*(): FormData {.importcpp: "new FormData()".} +proc append*(f: FormData, key: cstring, value: Blob) {.importcpp:"#.append(@)".} +proc append*(f: FormData, key: cstring, value: cstring) {.importcpp:"#.append(@)".} + +proc setRequestHeader*(r: HttpRequest; a, b: cstring) {.importcpp: "#.setRequestHeader(@)".} +proc statechange*(r: HttpRequest; cb: proc()) {.importcpp: "#.onreadystatechange = #".} +proc send*(r: HttpRequest; data: cstring) {.importcpp: "#.send(#)".} +proc send*(r: HttpRequest, data: Blob) {.importcpp: "#.send(#)".} +proc open*(r: HttpRequest; meth, url: cstring; async: bool) {.importcpp: "#.open(@)".} +proc newRequest*(): HttpRequest {.importcpp: "new XMLHttpRequest(@)".} + +when not declared(dom.File): + type + DomFile = ref FileObj + FileObj {.importc.} = object of Blob + lastModified: int + name: cstring +else: + type + DomFile = dom.File + +proc uploadFile*(url: cstring, file: Blob, onprogress :proc(data: ProgressEvent), + cont: proc (httpStatus: int; response: cstring); + headers: openarray[(cstring, cstring)] = []) = + proc contWrapper(httpStatus: int; response: cstring) = + cont(httpStatus, response) + + proc upload(r: HttpRequest):XMLHttpRequestUpload {.importcpp: "#.upload".} + var this {.importc: "this".}: ThisObj + + var formData = newFormData() + formData.append("upload_file",file) + formData.append("filename", DomFile(file).name) + let ajax = newRequest() + ajax.open("POST", url, true) + for a, b in items(headers): + ajax.setRequestHeader(a, b) + ajax.statechange proc() = + if this.readyState == 4: + contWrapper(this.status, this.responseText) + ajax.upload.onprogress = onprogress + ajax.send(formData.to(cstring)) + +proc ajax*(meth, url: cstring; headers: openarray[(cstring, cstring)]; + data: cstring; + cont: proc (httpStatus: int; response: cstring); + doRedraw: bool = true, + kxi: KaraxInstance = kxi, + useBinary: bool = false, + blob: Blob = nil) = + proc contWrapper(httpStatus: int; response: cstring) = + cont(httpStatus, response) + if doRedraw: redraw(kxi) + + var this {.importc: "this".}: ThisObj + + let ajax = newRequest() + ajax.open(meth, url, true) + for a, b in items(headers): + ajax.setRequestHeader(a, b) + ajax.statechange proc() = + if this.readyState == 4: + if this.status == 200: + contWrapper(this.status, this.responseText) + else: + contWrapper(this.status, this.responseText) + if useBinary: + ajax.send(blob) + else: + ajax.send(data) + +proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)]; + data: cstring; + cont: proc (httpStatus: int, response: cstring); + doRedraw: bool = true, + kxi: KaraxInstance = kxi) = + ajax("POST", url, headers, data, cont, doRedraw, kxi) + +proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)]; + data: Blob; + cont: proc (httpStatus: int, response: cstring); + doRedraw: bool = true, + kxi: KaraxInstance = kxi) = + ajax("POST", url, headers, "", cont, doRedraw, kxi, true, data) + +proc ajaxGet*(url: cstring; headers: openarray[(cstring, cstring)]; + cont: proc (httpStatus: int, response: cstring); + doRedraw: bool = true, + kxi: KaraxInstance = kxi) = + ajax("GET", url, headers, nil, cont, doRedraw, kxi) + +proc ajaxPut*(url: cstring; headers: openarray[(cstring, cstring)]; + data: cstring; + cont: proc (httpStatus: int, response: cstring); + doRedraw: bool = true, + kxi: KaraxInstance = kxi) = + ajax("PUT", url, headers, data, cont, doRedraw, kxi) + +proc ajaxDelete*(url: cstring; headers: openarray[(cstring, cstring)]; + cont: proc (httpStatus: int, response: cstring); + doRedraw: bool = true, + kxi: KaraxInstance = kxi) = + ajax("DELETE", url, headers, nil, cont, doRedraw, kxi) + + +proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".} +proc fromJson*[T](blob: cstring): T {.importc: "JSON.parse".} + diff --git a/karax/karax.nim b/karax/karax.nim new file mode 100644 index 0000000..2246676 --- /dev/null +++ b/karax/karax.nim @@ -0,0 +1,834 @@ +## Karax -- Single page applications for Nim. + +import kdom, vdom, jstrutils, compact, jdict, vstyles + +export kdom.Event, kdom.Blob + +when defined(nimNoNil): + {.experimental: "notnil".} + +proc kout*[T](x: T) {.importc: "console.log", varargs.} + ## The preferred way of debugging karax applications. + +type + PatchKind = enum + pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach, pkSame + Patch = object + k: PatchKind + parent, current: Node + newNode, oldNode: VNode + PatchV = object + parent, newChild: VNode + pos: int + ComponentPair = object + oldNode, newNode: VComponent + parent, current: Node + +type + RouterData* = ref object ## information that is passed to the 'renderer' callback + hashPart*: cstring ## the hash part of the URL for routing. + + KaraxInstance* = ref object ## underlying karax instance. Usually you don't have + ## know about this. + rootId: cstring + renderer: proc (data: RouterData): VNode {.closure.} + currentTree: VNode + postRenderCallback: proc (data: RouterData) + toFocus: Node + toFocusV: VNode + renderId: int + rendering: bool + patches: seq[Patch] # we reuse this to save allocations + patchLen: int + patchesV: seq[PatchV] + patchLenV: int + runCount: int + components: seq[ComponentPair] + surpressRedraws*: bool + byId: JDict[cstring, VNode] + when defined(stats): + recursion: int + orphans: JDict[cstring, bool] + + +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; enabled = true; kxi: KaraxInstance = kxi) = + if enabled: + kxi.toFocusV = n + +# ----------------- event wrapping --------------------------------------- + +template nativeValue(ev): cstring = cast[Element](ev.target).value +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) + # Do not call redraw() here! That is already done + # by ``karax.addEventHandler``. + +proc karaxEvents(d: Node): JSeq[(cstring, NativeEventHandler)] {.importcpp: "#.karaxEvents".} +proc `karaxEvents=`(d: Node; value: JSeq[(cstring, NativeEventHandler)]) {.importcpp: "#.karaxEvents = #".} + +proc addEventShell(d: Node; name: cstring; h: NativeEventHandler) = + # The DOM is such a pathetic piece of junk that it doesn't + # offer 'removeAllEventHandlers()'. Hence we store the event + # handler twice in 'd' so that we can emulate this properly. + # This is required to fix bug #139. + d.addEventListener(name, h) + if d.karaxEvents == nil: + d.karaxEvents = newJSeq[(cstring, NativeEventHandler)]() + d.karaxEvents.add((name, h)) + +proc removeAllEventHandlers(d: Node) = + if d.karaxEvents != nil: + for i in 0..= 0: + # result.key = n.key + for k, v in attrs(n): + if v != nil: + result.setAttr(k, v) + applyEvents(n) + if kxi != nil and 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; nesting = 0): bool = + if kxi.orphans.contains(n.id): return true + if n.kind == VNodeKind.component: + result = same(VComponent(n).expanded, e, nesting+1) + elif n.kind == VNodeKind.verbatim: + result = true + elif n.kind == VNodeKind.vthunk or n.kind == VNodeKind.dthunk: + # we don't check these: + result = true + elif toTag[n.kind] == e.nodename: + result = true + if n.kind != VNodeKind.text: + # BUGFIX: Microsoft's Edge gives the textarea a child containing the text node! + if e.len != n.len and n.kind != VNodeKind.textarea: + echo "expected ", n.len, " real ", e.len, " ", toTag[n.kind], " nesting ", nesting + return false + for i in 0 ..< n.len: + if not same(n[i], e[i], nesting+1): return false + else: + echo "VDOM: ", toTag[n.kind], " DOM: ", e.nodename + +proc replaceById(id: cstring; newTree: Node) = + let x = document.getElementById(id) + x.parentNode.replaceChild(newTree, x) + newTree.id = id + +type + EqResult = enum + componentsIdentical, different, similar, identical, usenewNode + +when defined(profileKarax): + type + DifferEnum = enum + deKind, deId, deIndex, deText, deComponent, deClass, + deSimilar + + 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; recursive: bool): EqResult = + 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: + when defined(profileKarax): inc reasons[deIndex] + return different + if a.kind == VNodeKind.text: + if a.text != b.text: + when defined(profileKarax): inc reasons[deText] + return different # similar + elif a.kind == VNodeKind.vthunk: + if a.text != b.text: return different + if a.len != b.len: return different + for i in 0..>" & depth) + elif n.kind == VNodeKind.text: + kout depth, n.text + for i in 0 ..< n.len: + printV(n[i], depth & " ") + +proc addPatch(kxi: KaraxInstance; ka: PatchKind; parenta, currenta: Node; + na, oldNode: VNode) = + let L = kxi.patchLen + if L >= kxi.patches.len: + # allocate more space: + kxi.patches.add(Patch(k: ka, parent: parenta, current: currenta, + newNode: na, oldNode: oldNode)) + else: + kxi.patches[L].k = ka + kxi.patches[L].parent = parenta + kxi.patches[L].current = currenta + kxi.patches[L].newNode = na + kxi.patches[L].oldNode = oldNode + 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 moveDom(dest, src: VNode) = + dest.dom = src.dom + src.dom = nil + if dest.id != nil: + kxi.byId[dest.id] = dest + assert dest.len == src.len + for i in 0.. We cannot ever use the old VDOM, we have to use the new virtual +# DOM. For identical nodes we need to take over the .dom field from the old +# node since we don't recompute them. This must be done recursively. In +# vnodeToDom we have to check whether the 'dom' field was already set. If so, +# There is nothing to do. +# "Similar" nodes can have the opposite effect; consider +# +# AAAABAAAA +# AAAACDAAAA +# +# In this example B did change to C and 'D' is new. However, replacing B by +# CD is fine. +# + + +proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance) = + when defined(stats): + if kxi.recursion > 100: + echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode, false) + if oldNode.kind == VNodeKind.text: + echo oldNode.text + inc kxi.recursion + let result = eq(newNode, oldNode, false) + case result + 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) + 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) + + let newLength = newNode.len + let oldLength = oldNode.len + if newLength == 0 and oldLength == 0: return + let minLength = min(newLength, oldLength) + + assert oldNode.kind == newNode.kind + var commonPrefix = 0 + + # compute common prefix: + while commonPrefix < minLength: + if eq(newNode[commonPrefix], oldNode[commonPrefix], true) == identical: + kxi.addPatch(pkSame, nil, nil, newNode[commonPrefix], oldNode[commonPrefix]) + inc commonPrefix + else: + break + + # compute common suffix: + var oldPos = oldLength - 1 + var newPos = newLength - 1 + while oldPos >= commonPrefix and newPos >= commonPrefix: + if eq(newNode[newPos], oldNode[oldPos], true) == identical: + kxi.addPatch(pkSame, nil, nil, newNode[newPos], oldNode[oldPos]) + dec oldPos + dec newPos + else: + break + + let pos = min(oldPos, newPos) + 1 + # now the different children are in commonPrefix .. pos - 1: + for i in commonPrefix..pos-1: + diff(newNode[i], oldNode[i], current, current.childNodes[i], kxi) + + if oldPos + 1 == oldLength: + for i in pos..newPos: + kxi.addPatch(pkAppend, current, nil, newNode[i], nil) + else: + let before = current.childNodes[oldPos + 1] + for i in pos..newPos: + kxi.addPatch(pkInsertBefore, current, before, newNode[i], nil) + # XXX call 'attach' here? + for i in pos..oldPos: + detach(oldNode[i]) + #doAssert i < current.childNodes.len + kxi.addPatch(pkRemove, current, current.childNodes[i], nil, nil) + of different: + detach(oldNode) + kxi.addPatch(pkReplace, parent, current, newNode, nil) + of usenewNode: doAssert(false, "eq returned usenewNode") + 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 + 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, nil) + when defined(karaxDebug): + echo "Component ", newNode.text, ": old expansion didn't exist" + else: + diff(x.expanded, oldExpanded, parent, current, kxi) + when false: + 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) + +when defined(stats): + proc depth(n: VNode; total: var int): int = + var m = 0 + for i in 0..= parent.len: + kxi.addPatch(pkAppend, current, nil, kid, nil) + parent.add(kid) + else: + let before = current.childNodes[position] + kxi.addPatch(pkInsertBefore, current, before, kid, nil) + parent.insert(kid, position) + applyPatch(kxi) + doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) + +proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) = + let olddom = oldNode.dom + doAssert olddom != nil + diff(newNode, oldNode, nil, olddom, kxi) + # this is a bit nasty: Since we cannot patch the 'parent' of + # the current VNode (because we don't store it at all!), we + # need to override the fields individually: + takeOverFields(newNode, oldNode) + applyComponents(kxi) + applyPatch(kxi) + if kxi.currentTree == oldNode: + kxi.currentTree = newNode + doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) + +var onhashChange {.importc: "window.onhashchange".}: proc() +var hashPart {.importc: "window.location.hash".}: cstring + +proc avoidDomDiffing*(kxi: KaraxInstance = kxi) = + ## enforce a full redraw for the next redraw operation. + ## This can be used as a temporary way to workaround DOM diffing + ## problems or to avoid the DOM diffing when you already know + ## it should use a completely new DOM. + ## This is an experimental API. + kxi.currentTree = nil + +proc reqFrame(callback: proc()): int {.importc: "window.requestAnimationFrame".} +when false: + proc cancelFrame(id: int) {.importc: "window.cancelAnimationFrame".} + +proc dodraw(kxi: KaraxInstance) = + if kxi.renderer.isNil: return + kxi.renderId = 0 + + if kxi.rendering: + # there is a render already running, delay 1 frame + kxi.renderId = reqFrame(proc () = kxi.dodraw) + return + + kxi.rendering = true + + let rdata = RouterData(hashPart: hashPart) + let newtree = kxi.renderer(rdata) + inc kxi.runCount + newtree.id = kxi.rootId + kxi.toFocus = nil + if kxi.currentTree == nil: + let asdom = toDom(newtree, useAttachedNode = true, kxi) + replaceById(kxi.rootId, asdom) + else: + doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) + let olddom = document.getElementById(kxi.rootId) + diff(newtree, kxi.currentTree, nil, olddom, kxi) + 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)) + + if not kxi.postRenderCallback.isNil: + kxi.postRenderCallback(rdata) + + # now that it's part of the DOM, give it the focus: + if kxi.toFocus != nil: + kxi.toFocus.focus() + kxi.rendering = false + when defined(stats): + kxi.recursion = 0 + var total = 0 + echo "depth ", depth(kxi.currentTree, total), " total ", total + +proc redraw*(kxi: KaraxInstance = kxi) = + # we buffer redraw requests: + when false: + if drawTimeout != nil: + clearTimeout(drawTimeout) + drawTimeout = setTimeout(dodraw, 30) + elif true: + if kxi.renderId == 0: + kxi.renderId = reqFrame(proc () = kxi.dodraw) + else: + dodraw(kxi) + +proc redrawSync*(kxi: KaraxInstance = kxi) = dodraw(kxi) + +proc init(ev: Event) = + kxi.renderId = reqFrame(proc () = kxi.dodraw) + +proc setRenderer*(renderer: proc (data: RouterData): VNode, + root: cstring = "ROOT", + clientPostRenderCallback: + proc (data: RouterData) = nil): KaraxInstance {. + discardable.} = + ## Setup Karax. Usually the return value can be ignored. + if document.getElementById(root).isNil: + let msg = "Could not find a
with id=" & root & + ". Karax needs it as its rendering target." + raise newException(Exception, $msg) + + result = KaraxInstance(rootId: root, renderer: renderer, + postRenderCallback: clientPostRenderCallback, + patches: newSeq[Patch](60), + patchesV: newSeq[PatchV](30), + components: @[], + surpressRedraws: false, + byId: newJDict[cstring, VNode](), + orphans: newJDict[cstring, bool]()) + kxi = result + window.onload = init + onhashChange = proc() = redraw() + +proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT", + clientPostRenderCallback: proc () = nil): KaraxInstance {.discardable.} = + ## Setup Karax. Usually the return value can be ignored. + proc wrapRenderer(data: RouterData): VNode = result = renderer() + proc wrapPostRender(data: RouterData) = + if clientPostRenderCallback != nil: clientPostRenderCallback() + setRenderer(wrapRenderer, root, wrapPostRender) + +proc setInitializer*(renderer: proc (data: RouterData): VNode, root: cstring = "ROOT", + clientPostRenderCallback: + proc (data: RouterData) = nil): KaraxInstance {.discardable.} = + ## Setup Karax. Usually the return value can be ignored. + result = KaraxInstance(rootId: root, renderer: renderer, + postRenderCallback: clientPostRenderCallback, + patches: newSeq[Patch](60), + patchesV: newSeq[PatchV](30), + components: @[], + surpressRedraws: true, + byId: newJDict[cstring, VNode](), + orphans: newJDict[cstring, bool]()) + kxi = result + window.onload = init + +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)`` + ## where ``wrapper`` calls the passed ``action`` and then triggers + ## a ``redraw``. + proc wrapper(ev: Event; n: VNode) = + action(ev, n) + if not kxi.surpressRedraws: redraw(kxi) + addEventListener(n, k, wrapper) + +proc addEventHandler*(n: VNode; k: EventKind; action: proc(); + kxi: KaraxInstance = kxi) = + ## Implements the foundation of Karax's event management. + ## Karax DSL transforms ``tag(onEvent = handler)`` to + ## ``tempNode.addEventHandler(tagNode, EventKind.onEvent, wrapper)`` + ## where ``wrapper`` calls the passed ``action`` and then triggers + ## a ``redraw``. + proc wrapper(ev: Event; n: VNode) = + action() + if not kxi.surpressRedraws: redraw(kxi) + addEventListener(n, k, wrapper) + +proc setOnHashChange*(action: proc (hashPart: cstring)) {.deprecated: "use setRenderer instead".} = + ## Now deprecated, instead pass a callback to ``setRenderer`` that receives + ## a ``data: RouterData`` parameter. + proc wrapper() = + action(hashPart) + redraw() + onhashchange = wrapper + +proc setForeignNodeId*(id: cstring; kxi: KaraxInstance = kxi) = + ## Declares a node ID as "foreign". Foreign nodes are not + ## under Karax's control in the sense that Karax does not attempt + ## to perform structural checks on them. + kxi.orphans[id] = true + +{.push stackTrace:off.} +proc setupErrorHandler*() = + ## Installs an error handler that transforms native JS unhandled + ## exceptions into Nim based stack traces. If `useAlert` is false, + ## the error message is put into the console, otherwise `alert` + ## is called. + proc stackTraceAsCstring(): cstring = cstring(getStackTrace()) + var onerror {.importc: "window.onerror", used.} = + proc (msg, url: cstring, line, col: int, error: cstring): bool = + var x = cstring"Error: " & msg & "\n" & stackTraceAsCstring() + echo(x) + return true # suppressErrorAlert +{.pop.} + +proc prepend(parent, kid: Element) = + parent.insertBefore(kid, parent.firstChild) + +proc loadScript*(jsfilename: cstring; kxi: KaraxInstance = kxi) = + let s = document.createElement("script") + s.setAttr "type", "text/javascript" + s.setAttr "src", jsfilename + document.body.prepend(s) + redraw(kxi) + +proc runLater*(action: proc(); later = 400): Timeout {.discardable.} = + proc wrapper() = + action() + redraw() + result = setTimeout(wrapper, later) + +proc setInputText*(n: VNode; s: cstring) = + ## Sets the text of input elements. + n.text = s + if n.dom != nil: n.dom.value = s + +proc getInputText*(n: VNode): cstring = + if n.dom != nil: + result = n.dom.value + +proc toChecked*(checked: bool): cstring = + (if checked: cstring"checked" else: cstring(nil)) + +proc toDisabled*(disabled: bool): cstring = + (if disabled: cstring"disabled" else: cstring(nil)) + +proc toSelected*(selected: bool): cstring = + (if selected: cstring"selected" else: cstring(nil)) + +proc vnodeToDom*(n: VNode; kxi: KaraxInstance = nil): Node = + result = toDom(n, useAttachedNode = false, kxi) diff --git a/karax/karaxdsl.nim b/karax/karaxdsl.nim new file mode 100644 index 0000000..451632c --- /dev/null +++ b/karax/karaxdsl.nim @@ -0,0 +1,200 @@ + +import macros, vdom, compact, kbase +from strutils import startsWith, toLowerAscii + +when defined(js): + import karax + +const + StmtContext = ["kout", "inc", "echo", "dec", "!"] + SpecialAttrs = ["id", "class", "value", "index", "style"] + +proc getName(n: NimNode): string = + case n.kind + of nnkIdent, nnkSym: + result = $n + of nnkAccQuoted: + result = "" + for i in 0.. 0: + result[L-1] = tcall2(result[L-1], tmpContext) + 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 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(evHandler(), tmpContext, + newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi")) + else: + result = n + of nnkVarSection, nnkLetSection, nnkConstSection: + result = n + of nnkCallKinds - {nnkInfix}: + let op = getName(n[0]) + let ck = isComponent(op) + if ck != ComponentKind.None: + let tmp = genSym(nskLet, "tmp") + let call = if ck == ComponentKind.Tag: + newCall(bindSym"tree", newDotExpr(bindSym"VNodeKind", n[0])) + elif ck == ComponentKind.VNode: + newCall(bindSym"vthunk", newLit(op)) + else: + newCall(bindSym"dthunk", newLit(op)) + result = newTree( + if tmpContext == nil: nnkStmtListExpr else: nnkStmtList, + newLetStmt(tmp, call)) + for i in 1 ..< n.len: + # named parameters are transformed into attributes or events: + let x = n[i] + if x.kind == nnkExprEqExpr: + let key = getName x[0] + if key.startsWith("on"): + result.add newCall(evHandler(), + tmp, newDotExpr(bindSym"EventKind", x[0]), x[1], ident("kxi")) + elif eqIdent(key, "style") and x[1].kind == nnkTableConstr: + result.add newDotAsgn(tmp, key, newCall("style", toKstring x[1])) + elif key in SpecialAttrs: + result.add newDotAsgn(tmp, key, x[1]) + if key == "value": + result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1]) + elif eqIdent(key, "setFocus"): + result.add newCall(key, tmp, x[1], ident"kxi") + elif eqIdent(key, "events"): + result.add newCall(bindSym"mergeEvents", tmp, x[1]) + else: + result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1]) + elif ck != ComponentKind.Tag: + call.add x + elif eqIdent(x, "setFocus"): + result.add newCall(x, tmp, bindSym"true", ident"kxi") + else: + result.add tcall2(x, tmp) + if tmpContext == nil: + result.add tmp + else: + result.add newCall(bindSym"add", tmpContext, tmp) + elif tmpContext != nil and op notin StmtContext: + var hasEventHandlers = false + for i in 1..`_. + +when not defined(js) and not defined(Nimdoc): + {.error: "This module only works on the JavaScript platform".} + +type + EventTarget* = ref EventTargetObj + EventTargetObj {.importc.} = object of RootObj + onabort*: proc (event: Event) {.nimcall.} + onblur*: proc (event: Event) {.nimcall.} + onchange*: proc (event: Event) {.nimcall.} + onclick*: proc (event: Event) {.nimcall.} + ondblclick*: proc (event: Event) {.nimcall.} + onerror*: proc (event: Event) {.nimcall.} + onfocus*: proc (event: Event) {.nimcall.} + onkeydown*: proc (event: Event) {.nimcall.} + onkeypress*: proc (event: Event) {.nimcall.} + onkeyup*: proc (event: Event) {.nimcall.} + onload*: proc (event: Event) {.nimcall.} + onmousedown*: proc (event: Event) {.nimcall.} + onmousemove*: proc (event: Event) {.nimcall.} + onmouseout*: proc (event: Event) {.nimcall.} + onmouseover*: proc (event: Event) {.nimcall.} + onmouseup*: proc (event: Event) {.nimcall.} + onreset*: proc (event: Event) {.nimcall.} + onselect*: proc (event: Event) {.nimcall.} + onsubmit*: proc (event: Event) {.nimcall.} + onunload*: proc (event: Event) {.nimcall.} + + # https://developer.mozilla.org/en-US/docs/Web/Events + DomEvent* {.pure.} = enum + Abort = "abort", + BeforeInput = "beforeinput", + Blur = "blur", + Click = "click", + CompositionEnd = "compositionend", + CompositionStart = "compositionstart", + CompositionUpdate = "compositionupdate", + DblClick = "dblclick", + Error = "error", + Focus = "focus", + FocusIn = "focusin", + FocusOut = "focusout", + Input = "input", + KeyDown = "keydown", + KeyPress = "keypress", + KeyUp = "keyup", + Load = "load", + MouseDown = "mousedown", + MouseEnter = "mouseenter", + MouseLeave = "mouseleave", + MouseMove = "mousemove", + MouseOut = "mouseout", + MouseOver = "mouseover", + MouseUp = "mouseup", + Resize = "resize", + Scroll = "scroll", + Select = "select", + Unload = "unload", + Wheel = "wheel" + + Window* = ref WindowObj + WindowObj {.importc.} = object of EventTargetObj + document*: Document + event*: Event + history*: History + location*: Location + closed*: bool + defaultStatus*: cstring + innerHeight*, innerWidth*: int + locationbar*: ref LocationBar + menubar*: ref MenuBar + name*: cstring + outerHeight*, outerWidth*: int + pageXOffset*, pageYOffset*: int + personalbar*: ref PersonalBar + scrollbars*: ref ScrollBars + statusbar*: ref StatusBar + status*: cstring + toolbar*: ref ToolBar + frames*: seq[Frame] + screen*: Screen + + Frame* = ref FrameObj + FrameObj {.importc.} = object of WindowObj + + ClassList* = ref ClassListObj + ClassListObj {.importc.} = object of RootObj + + NodeType* = enum + ElementNode = 1, + AttributeNode, + TextNode, + CDATANode, + EntityRefNode, + EntityNode, + ProcessingInstructionNode, + CommentNode, + DocumentNode, + DocumentTypeNode, + DocumentFragmentNode, + NotationNode + + Node* = ref NodeObj + NodeObj {.importc.} = object of EventTargetObj + attributes*: seq[Node] + childNodes*: seq[Node] + children*: seq[Node] + data*: cstring + firstChild*: Node + lastChild*: Node + nextSibling*: Node + nodeName*: cstring + nodeType*: NodeType + nodeValue*: cstring + parentNode*: Node + previousSibling*: Node + innerHTML*: cstring + style*: Style + when defined(nodejs): + id*: cstring + + Document* = ref DocumentObj + DocumentObj {.importc.} = object of NodeObj + alinkColor*: cstring + bgColor*: cstring + body*: Element + charset*: cstring + cookie*: cstring + defaultCharset*: cstring + fgColor*: cstring + head*: Element + lastModified*: cstring + linkColor*: cstring + referrer*: cstring + title*: cstring + URL*: cstring + vlinkColor*: cstring + anchors*: seq[AnchorElement] + forms*: seq[FormElement] + images*: seq[ImageElement] + applets*: seq[Element] + embeds*: seq[EmbedElement] + links*: seq[LinkElement] + + Element* = ref ElementObj + ElementObj {.importc.} = object of NodeObj + classList*: Classlist + checked*: bool + defaultChecked*: bool + defaultValue*: cstring + disabled*: bool + form*: FormElement + name*: cstring + readOnly*: bool + options*: seq[OptionElement] + selectedOptions*: seq[OptionElement] + clientWidth*, clientHeight*: int + contentEditable*: string + isContentEditable*: bool + dir*: string + offsetHeight*: int + offsetWidth*: int + offsetLeft*: int + offsetTop*: int + + # https://developer.mozilla.org/en-US/docs/Web/API/ValidityState + ValidityState* = ref ValidityStateObj + ValidityStateObj {.importc.} = object + badInput*: bool + customError*: bool + patternMismatch*: bool + rangeOverflow*: bool + rangeUnderflow*: bool + stepMismatch*: bool + tooLong*: bool + tooShort*: bool + typeMismatch*: bool + valid*: bool + valueMissing*: bool + + # https://developer.mozilla.org/en-US/docs/Web/API/Blob + Blob* = ref BlobObj + BlobObj {.importc.} = object of RootObj + size*: int + `type`*: cstring + + # https://developer.mozilla.org/en-US/docs/Web/API/File + File* = ref FileObj + FileObj {.importc.} = object of Blob + lastModified*: int + name*: cstring + + # https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement + InputElement* = ref InputElementObj + InputElementObj {.importc.} = object of Element + # Properties related to the parent form + formAction*: cstring + formEncType*: cstring + formMethod*: cstring + formNoValidate*: bool + formTarget*: cstring + + # Properties that apply to any type of input element that is not hidden + `type`*: cstring + autofocus*: bool + required*: bool + value*: cstring + validity*: ValidityState + validationMessage*: cstring + willValidate*: bool + + # Properties that apply only to elements of type "checkbox" or "radio" + indeterminate*: bool + + # Properties that apply only to elements of type "image" + alt*: cstring + height*: cstring + src*: cstring + width*: cstring + + # Properties that apply only to elements of type "file" + accept*: cstring + files*: seq[Blob] + + # Properties that apply only to text/number-containing or elements + autocomplete*: cstring + maxLength*: int + size*: int + pattern*: cstring + placeholder*: cstring + min*: cstring + max*: cstring + selectionStart*: int + selectionEnd*: int + selectionDirection*: cstring + + # Properties not yet categorized + dirName*: cstring + accessKey*: cstring + list*: Element + multiple*: bool + labels*: seq[Element] + step*: cstring + valueAsDate*: cstring + valueAsNumber*: float + + LinkElement* = ref LinkObj + LinkObj {.importc.} = object of ElementObj + target*: cstring + text*: cstring + x*: int + y*: int + + EmbedElement* = ref EmbedObj + EmbedObj {.importc.} = object of ElementObj + height*: int + hspace*: int + src*: cstring + width*: int + `type`*: cstring + vspace*: int + + AnchorElement* = ref AnchorObj + AnchorObj {.importc.} = object of ElementObj + text*: cstring + x*, y*: int + + OptionElement* = ref OptionObj + OptionObj {.importc.} = object of ElementObj + defaultSelected*: bool + selected*: bool + selectedIndex*: int + text*: cstring + value*: cstring + + # https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement + FormElement* = ref FormObj + FormObj {.importc.} = object of ElementObj + acceptCharset*: cstring + action*: cstring + autocomplete*: cstring + elements*: seq[Element] + encoding*: cstring + enctype*: cstring + length*: int + `method`*: cstring + noValidate*: bool + target*: cstring + + ImageElement* = ref ImageObj + ImageObj {.importc.} = object of ElementObj + border*: int + complete*: bool + height*: int + hspace*: int + lowsrc*: cstring + src*: cstring + vspace*: int + width*: int + + Style* = ref StyleObj + StyleObj {.importc.} = object of RootObj + background*: cstring + backgroundAttachment*: cstring + backgroundColor*: cstring + backgroundImage*: cstring + backgroundPosition*: cstring + backgroundRepeat*: cstring + border*: cstring + borderBottom*: cstring + borderBottomColor*: cstring + borderBottomStyle*: cstring + borderBottomWidth*: cstring + borderColor*: cstring + borderLeft*: cstring + borderLeftColor*: cstring + borderLeftStyle*: cstring + borderLeftWidth*: cstring + borderRight*: cstring + borderRightColor*: cstring + borderRightStyle*: cstring + borderRightWidth*: cstring + borderStyle*: cstring + borderTop*: cstring + borderTopColor*: cstring + borderTopStyle*: cstring + borderTopWidth*: cstring + borderWidth*: cstring + bottom*: cstring + captionSide*: cstring + clear*: cstring + clip*: cstring + color*: cstring + cursor*: cstring + direction*: cstring + display*: cstring + emptyCells*: cstring + cssFloat*: cstring + font*: cstring + fontFamily*: cstring + fontSize*: cstring + fontStretch*: cstring + fontStyle*: cstring + fontVariant*: cstring + fontWeight*: cstring + height*: cstring + left*: cstring + letterSpacing*: cstring + lineHeight*: cstring + listStyle*: cstring + listStyleImage*: cstring + listStylePosition*: cstring + listStyleType*: cstring + margin*: cstring + marginBottom*: cstring + marginLeft*: cstring + marginRight*: cstring + marginTop*: cstring + maxHeight*: cstring + maxWidth*: cstring + minHeight*: cstring + minWidth*: cstring + overflow*: cstring + padding*: cstring + paddingBottom*: cstring + paddingLeft*: cstring + paddingRight*: cstring + paddingTop*: cstring + pageBreakAfter*: cstring + pageBreakBefore*: cstring + pointerEvents*: cstring + position*: cstring + right*: cstring + scrollbar3dLightColor*: cstring + scrollbarArrowColor*: cstring + scrollbarBaseColor*: cstring + scrollbarDarkshadowColor*: cstring + scrollbarFaceColor*: cstring + scrollbarHighlightColor*: cstring + scrollbarShadowColor*: cstring + scrollbarTrackColor*: cstring + tableLayout*: cstring + textAlign*: cstring + textDecoration*: cstring + textIndent*: cstring + textTransform*: cstring + transform*: cstring + top*: cstring + verticalAlign*: cstring + visibility*: cstring + width*: cstring + wordSpacing*: cstring + zIndex*: int + + EventPhase* = enum + None = 0, + CapturingPhase, + AtTarget, + BubblingPhase + + # https://developer.mozilla.org/en-US/docs/Web/API/Event + Event* = ref EventObj + EventObj {.importc.} = object of RootObj + bubbles*: bool + cancelBubble*: bool + cancelable*: bool + composed*: bool + currentTarget*: Node + defaultPrevented*: bool + eventPhase*: int + target*: Node + `type`*: cstring + isTrusted*: bool + + # https://developer.mozilla.org/en-US/docs/Web/API/UIEvent + UIEvent* = ref UIEventObj + UIEventObj {.importc.} = object of Event + detail*: int64 + view*: Window + + # https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent + KeyboardEvent* = ref KeyboardEventObj + KeyboardEventObj {.importc.} = object of UIEvent + altKey*, ctrlKey*, metaKey*, shiftKey*: bool + code*: cstring + isComposing*: bool + key*: cstring + keyCode*: int + location*: int + + # https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values + KeyboardEventKey* {.pure.} = enum + # Modifier keys + Alt, + AltGraph, + CapsLock, + Control, + Fn, + FnLock, + Hyper, + Meta, + NumLock, + ScrollLock, + Shift, + Super, + Symbol, + SymbolLock, + + # Whitespace keys + ArrowDown, + ArrowLeft, + ArrowRight, + ArrowUp, + End, + Home, + PageDown, + PageUp, + + # Editing keys + Backspace, + Clear, + Copy, + CrSel, + Cut, + Delete, + EraseEof, + ExSel, + Insert, + Paste, + Redo, + Undo, + + # UI keys + Accept, + Again, + Attn, + Cancel, + ContextMenu, + Escape, + Execute, + Find, + Finish, + Help, + Pause, + Play, + Props, + Select, + ZoomIn, + ZoomOut, + + # Device keys + BrigtnessDown, + BrigtnessUp, + Eject, + LogOff, + Power, + PowerOff, + PrintScreen, + Hibernate, + Standby, + WakeUp, + + # Common IME keys + AllCandidates, + Alphanumeric, + CodeInput, + Compose, + Convert, + Dead, + FinalMode, + GroupFirst, + GroupLast, + GroupNext, + GroupPrevious, + ModeChange, + NextCandidate, + NonConvert, + PreviousCandidate, + Process, + SingleCandidate, + + # Korean keyboards only + HangulMode, + HanjaMode, + JunjaMode, + + # Japanese keyboards only + Eisu, + Hankaku, + Hiragana, + HiraganaKatakana, + KanaMode, + KanjiMode, + Katakana, + Romaji, + Zenkaku, + ZenkakuHanaku, + + # Function keys + F1, + F2, + F3, + F4, + F5, + F6, + F7, + F8, + F9, + F10, + F11, + F12, + F13, + F14, + F15, + F16, + F17, + F18, + F19, + F20, + Soft1, + Soft2, + Soft3, + Soft4, + + # Phone keys + AppSwitch, + Call, + Camera, + CameraFocus, + EndCall, + GoBack, + GoHome, + HeadsetHook, + LastNumberRedial, + Notification, + MannerMode, + VoiceDial, + + # Multimedia keys + ChannelDown, + ChannelUp, + MediaFastForward, + MediaPause, + MediaPlay, + MediaPlayPause, + MediaRecord, + MediaRewind, + MediaStop, + MediaTrackNext, + MediaTrackPrevious, + + # Audio control keys + AudioBalanceLeft, + AudioBalanceRight, + AudioBassDown, + AudioBassBoostDown, + AudioBassBoostToggle, + AudioBassBoostUp, + AudioBassUp, + AudioFaderFront, + AudioFaderRear, + AudioSurroundModeNext, + AudioTrebleDown, + AudioTrebleUp, + AudioVolumeDown, + AUdioVolumeMute, + AudioVolumeUp, + MicrophoneToggle, + MicrophoneVolumeDown, + MicrophoneVolumeMute, + MicrophoneVolumeUp, + + # TV control keys + TV, + TV3DMode, + TVAntennaCable, + TVAudioDescription, + TVAudioDescriptionMixDown, + TVAudioDescriptionMixUp, + TVContentsMenu, + TVDataService, + TVInput, + TVInputComponent1, + TVInputComponent2, + TVInputComposite1, + TVInputComposite2, + TVInputHDMI1, + TVInputHDMI2, + TVInputHDMI3, + TVInputHDMI4, + TVInputVGA1, + TVMediaContext, + TVNetwork, + TVNumberEntry, + TVPower, + TVRadioService, + TVSatellite, + TVSatelliteBS, + TVSatelliteCS, + TVSatelliteToggle, + TVTerrestrialAnalog, + TVTerrestrialDigital, + TVTimer, + + # Media controller keys + AVRInput, + AVRPower, + ColorF0Red, + ColorF1Green, + ColorF2Yellow, + ColorF3Blue, + ColorF4Grey, + ColorF5Brown, + ClosedCaptionToggle, + Dimmer, + DisplaySwap, + DVR, + Exit, + FavoriteClear0, + FavoriteClear1, + FavoriteClear2, + FavoriteClear3, + FavoriteRecall0, + FavoriteRecall1, + FavoriteRecall2, + FavoriteRecall3, + FavoriteStore0, + FavoriteStore1, + FavoriteStore2, + FavoriteStore3, + Guide, + GuideNextDay, + GuidePreviousDay, + Info, + InstantReplay, + Link, + ListProgram, + LiveContent, + Lock, + MediaApps, + MediaAudioTrack, + MediaLast, + MediaSkipBackward, + MediaSkipForward, + MediaStepBackward, + MediaStepForward, + MediaTopMenu, + NavigateIn, + NavigateNext, + NavigateOut, + NavigatePrevious, + NextFavoriteChannel, + NextUserProfile, + OnDemand, + Pairing, + PinPDown, + PinPMove, + PinPUp, + PlaySpeedDown, + PlaySpeedReset, + PlaySpeedUp, + RandomToggle, + RcLowBattery, + RecordSpeedNext, + RfBypass, + ScanChannelsToggle, + ScreenModeNext, + Settings, + SplitScreenToggle, + STBInput, + STBPower, + Subtitle, + Teletext, + VideoModeNext, + Wink, + ZoomToggle, + + # Speech recognition keys + SpeechCorrectionList, + SpeechInputToggle, + + # Document keys + Close, + New, + Open, + Print, + Save, + SpellCheck, + MailForward, + MailReply, + MailSend, + + # Application selector keys + LaunchCalculator, + LaunchCalendar, + LaunchContacts, + LaunchMail, + LaunchMediaPlayer, + LaunchMusicPlayer, + LaunchMyComputer, + LaunchPhone, + LaunchScreenSaver, + LaunchSpreadsheet, + LaunchWebBrowser, + LaunchWebCam, + LaunchWordProcessor, + LaunchApplication1, + LaunchApplication2, + LaunchApplication3, + LaunchApplication4, + LaunchApplication5, + LaunchApplication6, + LaunchApplication7, + LaunchApplication8, + LaunchApplication9, + LaunchApplication10, + LaunchApplication11, + LaunchApplication12, + LaunchApplication13, + LaunchApplication14, + LaunchApplication15, + LaunchApplication16, + + # Browser control keys + BrowserBack, + BrowserFavorites, + BrowserForward, + BrowserHome, + BrowserRefresh, + BrowserSearch, + BrowserStop, + + # Numeric keypad keys + Key11, + Key12, + Separator + + MouseButtons* = enum + NoButton = 0, + PrimaryButton = 1, + SecondaryButton = 2, + AuxilaryButton = 4, + FourthButton = 8, + FifthButton = 16 + + # https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + MouseEvent* = ref MouseEventObj + MouseEventObj {.importc.} = object of UIEvent + altKey*, ctrlKey*, metaKey*, shiftKey*: bool + button*: int + buttons*: int + clientX*, clientY*: int + movementX*, movementY*: int + offsetX*, offsetY*: int + pageX*, pageY*: int + relatedTarget*: EventTarget + #region*: cstring + screenX*, screenY*: int + x*, y*: int + + DataTransferItemKind* {.pure.} = enum + File = "file", + String = "string" + + # https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem + DataTransferItem* = ref DataTransferItemObj + DataTransferItemObj {.importc.} = object of RootObj + kind*: cstring + `type`*: cstring + + # https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer + DataTransfer* = ref DataTransferObj + DataTransferObj {.importc.} = object of RootObj + dropEffect*: cstring + effectAllowed*: cstring + files*: seq[Element] + items*: seq[DataTransferItem] + types*: seq[cstring] + + DataTransferDropEffect* {.pure.} = enum + None = "none", + Copy = "copy", + Link = "link", + Move = "move" + + DataTransferEffectAllowed* {.pure.} = enum + None = "none", + Copy = "copy", + CopyLink = "copyLink", + CopyMove = "copyMove", + Link = "link", + LinkMove = "linkMove", + Move = "move", + All = "all", + Uninitialized = "uninitialized" + + DragEventTypes* = enum + Drag = "drag", + DragEnd = "dragend", + DragEnter = "dragenter", + DragExit = "dragexit", + DragLeave = "dragleave", + DragOver = "dragover", + DragStart = "dragstart", + Drop = "drop" + + # https://developer.mozilla.org/en-US/docs/Web/API/DragEvent + DragEvent* {.importc.} = object of MouseEvent + dataTransfer*: DataTransfer + + TouchList* {.importc.} = ref object of RootObj + length*: int + + Touch* = ref TouchObj + TouchObj {.importc.} = object of RootObj + identifier*: int + screenX*, screenY*, clientX*, clientY*, pageX*, pageY*: int + target*: Element + radiusX*, radiusY*: int + rotationAngle*: int + force*: float + + TouchEvent* = ref TouchEventObj + TouchEventObj {.importc.} = object of UIEvent + changedTouches*, targetTouches*, touches*: seq[Touch] + + Location* = ref LocationObj + LocationObj {.importc.} = object of RootObj + hash*: cstring + host*: cstring + hostname*: cstring + href*: cstring + pathname*: cstring + port*: cstring + protocol*: cstring + search*: cstring + + History* = ref HistoryObj + HistoryObj {.importc.} = object of RootObj + length*: int + + Navigator* = ref NavigatorObj + NavigatorObj {.importc.} = object of RootObj + appCodeName*: cstring + appName*: cstring + appVersion*: cstring + cookieEnabled*: bool + language*: cstring + platform*: cstring + userAgent*: cstring + mimeTypes*: seq[ref MimeType] + + Plugin* {.importc.} = object of RootObj + description*: cstring + filename*: cstring + name*: cstring + + MimeType* {.importc.} = object of RootObj + description*: cstring + enabledPlugin*: ref Plugin + suffixes*: seq[cstring] + `type`*: cstring + + LocationBar* {.importc.} = object of RootObj + visible*: bool + MenuBar* = LocationBar + PersonalBar* = LocationBar + ScrollBars* = LocationBar + ToolBar* = LocationBar + StatusBar* = LocationBar + + Screen = ref ScreenObj + ScreenObj {.importc.} = object of RootObj + availHeight*: int + availWidth*: int + colorDepth*: int + height*: int + pixelDepth*: int + width*: int + + TimeOut* {.importc.} = ref object of RootObj + Interval* {.importc.} = object of RootObj + + AddEventListenerOptions* = object + capture*: bool + once*: bool + passive*: bool + +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..= 0 and rect.left >= 0 and + 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.} +proc offsetTop*(e: Node): int {.importcpp: "#.offsetTop", nodecl.} +proc offsetLeft*(e: Node): int {.importcpp: "#.offsetLeft", nodecl.} diff --git a/karax/languages.nim b/karax/languages.nim new file mode 100644 index 0000000..6b6b027 --- /dev/null +++ b/karax/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"] diff --git a/karax/localstorage.nim b/karax/localstorage.nim new file mode 100644 index 0000000..e2fb62e --- /dev/null +++ b/karax/localstorage.nim @@ -0,0 +1,10 @@ +## Karax -- Single page applications for Nim. + +## This module contains wrappers for the HTML 5 local storage. + +proc getItem*(key: cstring): cstring {.importc: "localStorage.getItem".} +proc setItem*(key, value: cstring) {.importc: "localStorage.setItem".} +proc hasItem*(key: cstring): bool {.importcpp: "(localStorage.getItem(#) !== null)".} +proc clear*() {.importc: "localStorage.clear".} + +proc removeItem*(key: cstring) {.importc: "localStorage.removeItem".} diff --git a/karax/lookuptables.nim b/karax/lookuptables.nim new file mode 100644 index 0000000..1a1d51d --- /dev/null +++ b/karax/lookuptables.nim @@ -0,0 +1,38 @@ + +import macros, kbase +from strutils import `%` + +macro fieldNamesAsArray*(t: typed; pattern = "$1"): untyped = + var impl = getTypeImpl(getTypeImpl(t)[1]) + if impl.kind == nnkRefTy: impl = getTypeImpl(impl[0]) + expectKind impl, nnkObjectTy + impl = impl[2] + expectKind impl, nnkRecList + result = newTree(nnkBracket) + for x in impl: + expectKind x, nnkIdentDefs + result.add(newCall(bindSym"kstring", newLit(pattern.strVal % $x[0]))) + +proc str(n: NimNode): NimNode = + if n.kind in {nnkStrLit, nnkTripleStrLit}: + result = newCall(bindSym"kstring", n) + else: + result = copyNimNode(n) + for i in 0..= 0: + x.s.delete(position) + x.broadcast(Mark) + x.broadcast(Deleted, position) + +when false: + proc `:=`[T](x: Reactive[T], f: proc(): T) = + toTrack = wrapObserver(f()) + x.value = f() + toTrack = nil + + 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..