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 index 8298ba9..3eec1a4 100644 --- a/docs/docs.rst +++ b/docs/docs.rst @@ -10,17 +10,17 @@ and some extensions that allow for an efficient component system. These extensions are: ``VNodeKind.int`` - The node havs a single integer field. + The node has a single integer field. ``VNodeKind.bool`` The node has a single boolean field. -``VNodeKint.vthunk`` +``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. -``VNodeKint.dthunk`` +``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. @@ -53,6 +53,6 @@ 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)``. +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 index 18179c1..c84b68b 100644 --- a/examples/mediaplayer/mediaplayer.nim +++ b/examples/mediaplayer/mediaplayer.nim @@ -1,5 +1,5 @@ -import karax, karaxdsl, vdom, dom, components +import karax / [karax, karaxdsl, vdom, kdom, compact] const Play = 0 @@ -12,10 +12,10 @@ proc play(n: Node) {.importcpp.} proc pause(n: Node) {.importcpp.} proc `width=`(n: Node, w: int) {.importcpp: "#.width = #".} -proc mplayer*(id, resource: cstring): VNode {.component.} = +proc mplayer*(id, resource: cstring): VNode {.compact.} = proc handler(ev: Event; n: VNode) = let myVideo = document.getElementById(id) - case n.key + case n.index of Play: if myVideo.paused: myVideo.play() @@ -27,13 +27,13 @@ proc mplayer*(id, resource: cstring): VNode {.component.} = else: discard result = buildHtml(tdiv): - button(onclick=handler, key=Play): + button(onclick=handler, index=Play): text "Play/Pause" - button(onclick=handler, key=Big): + button(onclick=handler, index=Big): text "Big" - button(onclick=handler, key=Small): + button(onclick=handler, index=Small): text "Small" - button(onclick=handler, key=Normal): + button(onclick=handler, index=Normal): text "Normal" br() br() diff --git a/examples/mediaplayer/playerapp.html b/examples/mediaplayer/playerapp.html index 9e076e6..a45a2e8 100644 --- a/examples/mediaplayer/playerapp.html +++ b/examples/mediaplayer/playerapp.html @@ -1,15 +1,15 @@ - - Player app - + + Player app + - + -
+
- + - + diff --git a/examples/mediaplayer/playerapp.nim b/examples/mediaplayer/playerapp.nim index e611eec..acb2e68 100644 --- a/examples/mediaplayer/playerapp.nim +++ b/examples/mediaplayer/playerapp.nim @@ -1,6 +1,6 @@ ## Example app that shows how to write and embed a custom component. -include karaxprelude +include karax/prelude import mediaplayer const url = "https://www.w3schools.com/html/mov_bbb.mp4" diff --git a/examples/mediaplayer/playerapp.nims b/examples/mediaplayer/playerapp.nims index 9028ff1..cf2eb9e 100644 --- a/examples/mediaplayer/playerapp.nims +++ b/examples/mediaplayer/playerapp.nims @@ -1 +1 @@ ---path: "../../src" +--path: "../.." diff --git a/examples/scrollapp/scrollapp.html b/examples/scrollapp/scrollapp.html index 1ff8b1b..44d657e 100644 --- a/examples/scrollapp/scrollapp.html +++ b/examples/scrollapp/scrollapp.html @@ -1,15 +1,15 @@ - - Todo app - + + Todo app + - + -
+
- + - + diff --git a/examples/scrollapp/scrollapp.nim b/examples/scrollapp/scrollapp.nim index e5ef9c1..4490447 100644 --- a/examples/scrollapp/scrollapp.nim +++ b/examples/scrollapp/scrollapp.nim @@ -1,7 +1,7 @@ ## Example that shows how to accomplish an "infinitely scrolling" app. -include karaxprelude -import jstrutils, dom +include karax/prelude +import karax / [jstrutils, kdom, vstyles] var entries: seq[cstring] = @[] for i in 1..500: @@ -16,7 +16,8 @@ proc scrollEvent(ev: Event; n: VNode) = proc createDom(): VNode = result = buildHtml(): - tdiv(onscroll=scrollEvent, style="height: 400px; overflow: scroll"): + tdiv(onscroll=scrollEvent, style=style( + (StyleAttr.height, cstring"400px"), (StyleAttr.overflow, cstring"scroll"))): for x in entries: tdiv: text x diff --git a/examples/scrollapp/scrollapp.nims b/examples/scrollapp/scrollapp.nims index 9028ff1..cf2eb9e 100644 --- a/examples/scrollapp/scrollapp.nims +++ b/examples/scrollapp/scrollapp.nims @@ -1 +1 @@ ---path: "../../src" +--path: "../.." diff --git a/examples/todoapp/todoapp.html b/examples/todoapp/todoapp.html index 488a3bf..ab3daa7 100644 --- a/examples/todoapp/todoapp.html +++ b/examples/todoapp/todoapp.html @@ -1,16 +1,16 @@ - - Todo app - - + + Todo app + + - + -
+
- + - + diff --git a/examples/todoapp/todoapp.nim b/examples/todoapp/todoapp.nim index 1ba06a6..e3720fd 100644 --- a/examples/todoapp/todoapp.nim +++ b/examples/todoapp/todoapp.nim @@ -1,5 +1,5 @@ -import vdom, karax, karaxdsl, jstrutils, components +import karax / [vdom, karax, karaxdsl, jstrutils, compact, localstorage] type Filter = enum @@ -9,91 +9,88 @@ var selectedEntry = -1 filter: Filter entriesLen: int + doneswitch = true +const + contentSuffix = cstring"content" + completedSuffix = cstring"completed" + lenSuffix = cstring"entriesLen" -proc getItem(key : cstring ): cstring {.importc: "localStorage.getItem"} -proc setItem(key, value : cstring ) {.importc: "localStorage.setItem"} -proc clearEntries() {.importc: "localStorage.clear"} - -proc getEntryCstring(pos: int): cstring = - result = getItem(&pos & cstring"cstring") +proc getEntryContent(pos: int): cstring = + result = getItem(&pos & contentSuffix) if result == cstring"null": result = nil proc isCompleted(pos: int): bool = - var value = getItem(cstring($pos & "bool")) + var value = getItem(&pos & completedSuffix) result = value == cstring"true" -proc setEntryCstring(pos: int, value: cstring) = - setItem(cstring($pos & "cstring"), value) +proc setEntryContent(pos: int, content: cstring) = + setItem(&pos & contentSuffix, content) -proc markAsCompleted(pos: int, value : bool) = - var val = cstring"true" - if not value: - val = cstring"false" - setItem(cstring($pos & "bool"), val) +proc markAsCompleted(pos: int, completed: bool) = + setItem(&pos & completedSuffix, &completed) -proc addEntry(str: cstring, bval : bool) = - setEntryCstring(entriesLen, str) - markAsCompleted(entriesLen, bval) +proc addEntry(content: cstring, completed: bool) = + setEntryContent(entriesLen, content) + markAsCompleted(entriesLen, completed) inc entriesLen + setItem(lenSuffix, &entriesLen) -proc updateEntry(pos: int, str: cstring, bval: bool) = - setEntryCstring(pos, str) - markAsCompleted(pos, bval) +proc updateEntry(pos: int, content: cstring, completed: bool) = + setEntryContent(pos, content) + markAsCompleted(pos, completed) proc onTodoEnter(ev: Event; n: VNode) = - addEntry(n.value, false) - n.value = "" + if n.value.strip() != "": + addEntry(n.value, false) + n.value = "" proc removeHandler(ev: Event; n: VNode) = - updateEntry(n.key, cstring(nil), false) + updateEntry(n.index, cstring(nil), false) proc editHandler(ev: Event; n: VNode) = - selectedEntry = n.key + selectedEntry = n.index proc focusLost(ev: Event; n: VNode) = selectedEntry = -1 proc editEntry(ev: Event; n: VNode) = - setEntryCstring(n.key, n.value) + setEntryContent(n.index, n.value) selectedEntry = -1 proc toggleEntry(ev: Event; n: VNode) = - let id = n.key + let id = n.index markAsCompleted(id, not isCompleted(id)) proc onAllDone(ev: Event; n: VNode) = - clearEntries() - 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 2303d95..0000000 --- a/experiments/example.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - Todo app - - - - -
- - - - - diff --git a/experiments/example.nim b/experiments/example.nim deleted file mode 100644 index 3fe4000..0000000 --- a/experiments/example.nim +++ /dev/null @@ -1,32 +0,0 @@ - -import vdom, components, karax, karaxdsl, jdict, jstrutils - -var - images: seq[cstring] = @[cstring"a", "b", "c", "d"] - -proc carousel*(key: VKey): VNode {.component.} = - state: - var counter: int = 0 - - proc next(ev: Event; n: VNode) = - counter = (counter + 1) mod images.len - - result = buildHtml(tdiv(key=key)): - text images[counter] - button(onclick = next): - text "Next" - -proc createDom(): VNode = - result = buildHtml(table): - tr: - td: - carousel(0) - td: - carousel(1) - tr: - td: - carousel(2) - td: - carousel(3) - -setRenderer createDom diff --git a/experiments/example.nims b/experiments/example.nims deleted file mode 100644 index 6b5771c..0000000 --- a/experiments/example.nims +++ /dev/null @@ -1 +0,0 @@ ---path: "../src" diff --git a/experiments/menus.nim b/experiments/menus.nim new file mode 100644 index 0000000..6ea0529 --- /dev/null +++ b/experiments/menus.nim @@ -0,0 +1,65 @@ +## Example program that shows how to create menus with Karax. + +include prelude +import jstrutils, kdom + +proc contentA(): VNode = + result = buildHtml(tdiv): + text "content A" + +proc contentB(): VNode = + result = buildHtml(tdiv): + text "content B" + +proc contentC(): VNode = + result = buildHtml(tdiv): + text "content C" + + + +type + MenuItemHandler = proc(): VNode + +var content: MenuItemHandler = contentA + +proc menuAction(x: MenuItemHandler): proc() = + result = proc() = content = x + +proc buildMenu(): VNode = + result = buildHtml(tdiv): + nav(class="navbar is-primary"): + tdiv(class="navbar-brand"): + a(class="navbar-item", onclick = menuAction(contentA)): + strong: + text "My awesome menu" + + tdiv(id="navMenuTransparentExample", class="navbar-menu"): + tdiv(class = "navbar-start"): + tdiv(class="navbar-item has-dropdown is-hoverable"): + a(class="navbar-link", onclick = menuAction(contentB)): + text "Masters" + tdiv(class="navbar-dropdown is-boxed"): + a(class="navbar-item", onclick = menuAction(contentC)): + text "Inventory" + a(class="navbar-item", onclick = menuAction(contentC)): + text "Product" + hr(class="navbar-divider"): + a(class="navbar-item", onclick = menuAction(contentC)): + text "Product" + + tdiv(class="navbar-item has-dropdown is-hoverable"): + a(class="navbar-link", onclick = menuAction(contentC)): + text "Transactions" + tdiv(class = "navbar-dropdown is-boxed", id="blogDropdown"): + a(class="navbar-item", onclick = menuAction(contentC)): + text "Purchase Bill" + a(class="navbar-item", onclick = menuAction(contentC)): + text "Purchase Return" + a(class="navbar-item", onclick = menuAction(contentC)): + text "Sale Bill" + hr(class="navbar-divider"): + a(class="navbar-item", onclick = menuAction(contentC)): + text "Stock Adjustment" + content() + +setRenderer buildMenu diff --git a/experiments/nativenodes.nim b/experiments/nativenodes.nim new file mode 100644 index 0000000..9d255ee --- /dev/null +++ b/experiments/nativenodes.nim @@ -0,0 +1,18 @@ + + +include karax / prelude +import karax / kdom + +proc myInput: VNode = + result = buildHtml: + input() + +var inp = vnodeToDom(myInput()) + +proc myAwesomeComponent(x: Node): VNode = + result = dthunk(x) + +proc main: VNode = + result = myAwesomeComponent(inp) + +setRenderer main diff --git a/experiments/nextgen.nim b/experiments/nextgen.nim new file mode 100644 index 0000000..56330d0 --- /dev/null +++ b/experiments/nextgen.nim @@ -0,0 +1,120 @@ + +import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, reactive + +proc textInput*(text: RString; focus: RBool): VNode {.track.} = + proc onFlip(ev: Event; target: VNode) = + focus <- not focus.value + + proc onKeyupEnter(ev: Event; target: VNode) = + text <- target.value + + proc onkeyup(ev: Event; n: VNode) = + # keep displayValue up to date, but do not tell the client yet! + text.value = n.value + + result = buildHtml(input(`type`="text", + value=text.value, onblur=onFlip, onfocus=onFlip, + onkeyupenter=onkeyupenter, onkeyup=onkeyup, setFocus=focus.value)) + +var + errmsg = rstr("") + +makeReactive: + type + User = ref object + firstname, lastname: cstring + selected: bool + +var gu = newRSeq(@[ (User(rawFirstname: "Some", rawLastname: "Body")), + (User(rawFirstname: "Some", rawLastname: "One")), + (User(rawFirstname: "Some", rawLastname: "Two"))]) +var prevSelected: User = nil #newReactive[User](nil) + +proc unselect() = + if prevSelected != nil: + prevSelected.selected = false + prevSelected = nil + +proc select(u: User) = + unselect() + u.selected = true + prevSelected = u + +proc toUI*(isFirstname: bool): RString = + result = RString() + result.subscribe proc (v: cstring) = + if v.len > 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/refs/demo.html b/experiments/refs/demo.html new file mode 100644 index 0000000..941333a --- /dev/null +++ b/experiments/refs/demo.html @@ -0,0 +1,13 @@ + + + + + + Refs + + + +
+ + + diff --git a/experiments/refs/demo.nim b/experiments/refs/demo.nim new file mode 100644 index 0000000..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 f9c8ccb..86a802f 100644 --- a/experiments/scrollapp.nim +++ b/experiments/scrollapp.nim @@ -1,5 +1,5 @@ -import vdom, karax, karaxdsl, jdict, jstrutils, components +import vdom, karax, karaxdsl, jdict, jstrutils, compact type Filter = enum diff --git a/experiments/scrollapp.nims b/experiments/scrollapp.nims deleted file mode 100644 index 532b206..0000000 --- a/experiments/scrollapp.nims +++ /dev/null @@ -1 +0,0 @@ ---path: "../src" 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/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/src/karaxdsl.nim b/karax/karaxdsl.nim similarity index 51% rename from src/karaxdsl.nim rename to karax/karaxdsl.nim index ade1c24..451632c 100644 --- a/src/karaxdsl.nim +++ b/karax/karaxdsl.nim @@ -1,25 +1,46 @@ -import macros, karax, vdom, components +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", "key"] + SpecialAttrs = ["id", "class", "value", "index", "style"] proc getName(n: NimNode): string = case n.kind - of nnkIdent: - result = $n.ident + 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, nnkCaseStmt, - nnkTryStmt, nnkFinally: + of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkTryStmt, + nnkFinally: # recurse for every child: result = copyNimNode(n) for x in n: result.add tcall2(x, tmpContext) + of nnkCaseStmt: + # recurse for children, but don't add call for case ident + result = copyNimNode(n) + result.add n[0] + for i in 1 ..< n.len: + result.add tcall2(n[i], tmpContext) + of 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: + of nnkCallKinds - {nnkInfix}: let op = getName(n[0]) let ck = isComponent(op) if ck != ComponentKind.None: @@ -75,16 +120,24 @@ proc tcall2(n, tmpContext: NimNode): NimNode = if x.kind == nnkExprEqExpr: let key = getName x[0] if key.startsWith("on"): - result.add newCall(bindSym"addEventHandler", - tmp, newDotExpr(bindSym"EventKind", x[0]), x[1]) + 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) + result.add newCall(x, tmp, bindSym"true", ident"kxi") else: result.add tcall2(x, tmp) if tmpContext == nil: @@ -92,7 +145,28 @@ proc tcall2(n, tmpContext: NimNode): NimNode = else: result.add newCall(bindSym"add", tmpContext, tmp) elif tmpContext != nil and op notin StmtContext: - result = newCall(bindSym"add", tmpContext, n) + var hasEventHandlers = false + for i in 1..`_. + +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..