├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── LICENSE.txt
├── docs
├── benchmark.png
└── docs.rst
├── examples
├── ajax
│ ├── ajax.nim
│ └── styles.css
├── button.nim
├── buttonlambda.nim
├── carousel
│ ├── carousel.html
│ ├── carousel.nim
│ └── carousel.nims
├── config.nims
├── hellostyle.nim
├── hellouniverse.nim
├── helloworld.nim
├── karaxtmulti
│ ├── README.md
│ ├── app1.nim
│ ├── app2.nim
│ └── index.html
├── login.nim
├── mediaplayer
│ ├── mediaplayer.nim
│ ├── playerapp.html
│ ├── playerapp.nim
│ └── playerapp.nims
├── scrollapp
│ ├── scrollapp.html
│ ├── scrollapp.nim
│ └── scrollapp.nims
├── slider.nim
├── todoapp
│ ├── style.css
│ ├── todoapp.html
│ ├── todoapp.nim
│ └── todoapp.nims
└── toychat.nim
├── experiments
├── ajaxtest.html
├── ajaxtest.nim
├── config.nims
├── dyn.nim
├── echartstest.html
├── echartstest.nim
├── menus.nim
├── nativenodes.nim
├── nextgen.nim
├── oldwidgets.nim
├── refs
│ ├── demo.html
│ ├── demo.nim
│ └── demo.nims
├── scrollapp.html
├── scrollapp.nim
├── scrollapp_simple.nim
├── style.css
└── trello
│ ├── knete.nim
│ ├── trello.nim
│ └── widgets.nim
├── guide
├── Installation.md
└── Introduction.md
├── karax.nimble
├── karax
├── autocomplete.nim
├── compact.nim
├── errors.nim
├── i18n.nim
├── jdict.nim
├── jjson.nim
├── jstrutils.nim
├── jwebsockets.nim
├── kajax.nim
├── karax.nim
├── karaxdsl.nim
├── kbase.nim
├── kdom.nim
├── kdom_impl.nim
├── languages.nim
├── localstorage.nim
├── lookuptables.nim
├── prelude.nim
├── reactive.nim
├── shash.nim
├── tools
│ ├── karun.nim
│ ├── karun.nims
│ └── static_server.nim
├── vdom.nim
├── vstyles.nim
└── xdom.nim
├── readme.md
└── tests
├── blur.nim
├── compiler_tests.nim
├── components.nim
├── diffDomTests.html
├── diffDomTests.nim
├── difftest.nim
├── domEventTests.css
├── domEventTests.html
├── domEventTests.nim
├── jquery-3.2.1.js
├── lists.nim
├── nativehtmlgen.nim
├── nim.cfg
├── scope.nim
├── tester.nim
└── xmlNodeConversionTests.nim
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: CI
4 |
5 | # Controls when the action will run.
6 | on:
7 | # Triggers the workflow on push or pull request events but only for the master branch
8 | push:
9 | branches: [ master ]
10 | pull_request:
11 | branches: [ master ]
12 |
13 | # Allows you to run this workflow manually from the Actions tab
14 | workflow_dispatch:
15 |
16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
17 | jobs:
18 | # This workflow contains a single job called "build"
19 | build:
20 | # The type of runner that the job will run on
21 | runs-on: ubuntu-latest
22 | container:
23 | image: nimlang/nim
24 | options: --user 1001
25 | # Steps represent a sequence of tasks that will be executed as part of the job
26 | steps:
27 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
28 | - uses: actions/checkout@v4
29 | # Runs a single command using the runners shell
30 | - name: Tests
31 | run: |
32 | nimble develop -y
33 | nim c -r tests/tester.nim
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | nimcache/
2 | /tests/tester
3 | karax/tools/karun
4 | *.code-workspace
5 | *.exe
6 | app.js
7 | app.html
8 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Xored Software, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/docs/benchmark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karaxnim/karax/a1eaaa87af756c21db4524c38cad4a004218f740/docs/benchmark.png
--------------------------------------------------------------------------------
/docs/docs.rst:
--------------------------------------------------------------------------------
1 |
2 | Virtual DOM
3 | ===========
4 |
5 | The virtual dom is in the ``vdom`` module.
6 |
7 | The ``VNodeKind`` enum describes every tag that the HTML 5 spec includes
8 | and some extensions that allow for an efficient component system.
9 |
10 | These extensions are:
11 |
12 | ``VNodeKind.int``
13 | The node has a single integer field.
14 |
15 | ``VNodeKind.bool``
16 | The node has a single boolean field.
17 |
18 | ``VNodeKind.vthunk``
19 | The node is a `virtual thunk`:idx:. This means there is a
20 | function attached to it that produces the ``VNode`` structure
21 | on demand.
22 |
23 | ``VNodeKind.dthunk``
24 | The node is a `DOM thunk`:idx:. This means there is a
25 | function attached to it that produces the ``Node`` DOM structure
26 | on demand.
27 |
28 | For convenience of the resulting Nim DSL these tags have enum names
29 | that differ from their HTML equivalent:
30 |
31 | ================= =======================================================
32 | Enum value HTML
33 | ================= =======================================================
34 | ``tdiv`` ``div`` (changed because ``div`` is a keyword in Nim)
35 | ``italic`` ``i``
36 | ``bold`` ``b``
37 | ``strikethrough`` ``s``
38 | ``underlined`` ``u``
39 | ================= =======================================================
40 |
41 |
42 | A virtual dom node also has a special field set called ``key``, an integer
43 | that can be used as a data model specific key/id. It can be accessed in event
44 | handlers to change the data model. See the todoapp for an example of how to
45 | use it.
46 |
47 | **Note**: This is not a hint for the DOM diff algorithm, multipe nodes can
48 | all have the same key value.
49 |
50 |
51 | Event system
52 | ============
53 |
54 | Karax does not abstract over the event system the DOM offers much: The same
55 | ``Event`` structure is used. Every callback has the
56 | signature ``proc (ev: Event; n: VNode)`` or empty ``proc ()``.
57 |
58 |
59 |
--------------------------------------------------------------------------------
/examples/ajax/ajax.nim:
--------------------------------------------------------------------------------
1 |
2 | ##[
3 | An example that uses ajax to load the nim package list and display a simple searchable index
4 | ]##
5 |
6 | import karax/[karax, karaxdsl, vdom, jstrutils, kajax, jjson]
7 |
8 | type
9 | Package = object
10 | name, description, url: cstring
11 |
12 | Progress = enum
13 | Loading,
14 | Loaded
15 |
16 | AppState = object
17 | progress: Progress
18 | packages: seq[Package]
19 | searchText: cstring
20 |
21 | var state = AppState()
22 |
23 | proc init =
24 | # start a request to get the package list from github
25 | ajaxGet("https://raw.githubusercontent.com/nim-lang/packages/master/packages.json", @[], proc (status: int, response: cstring) =
26 | for json in parse(response):
27 | # only add entries that aren't aliases
28 | if not json.hasField("alias"):
29 | state.packages.add(Package(
30 | name: json["name"].getStr(),
31 | description: json["description"].getStr(),
32 | url: json["url"].getStr()
33 | ))
34 | state.progress = Loaded
35 | )
36 |
37 | proc drawPackage(package: Package): VNode =
38 | buildHtml(tdiv(class="result")):
39 | h2:
40 | a(href=package.url):
41 | text package.name
42 | p: text package.description
43 |
44 | proc loadingView: VNode =
45 | buildHtml(tdiv):
46 | h1: text "Loading..."
47 |
48 | proc searchView: VNode =
49 | buildHtml(tdiv):
50 | h1: text "Search"
51 | input:
52 | proc onkeyup(e: Event, n: VNode) =
53 | state.searchText = n.value
54 |
55 | # we show up to 50 packages that match the search text. If the input is
56 | # empty, we just show the first 50 packages in the list
57 | var found = 0
58 |
59 | for package in state.packages:
60 | if state.searchText.len == 0 or package.name.contains(state.searchText) or package.description.contains(state.searchText):
61 | drawPackage(package)
62 | inc found
63 | if found > 50:
64 | break
65 |
66 | proc main: VNode =
67 | if state.progress == Loading:
68 | loadingView()
69 | else:
70 | searchView()
71 |
72 | setRenderer(main)
73 | init()
74 |
--------------------------------------------------------------------------------
/examples/ajax/styles.css:
--------------------------------------------------------------------------------
1 |
2 | body {
3 | max-width: 60em;
4 | margin: 0 auto;
5 | font-family: sans-serif;
6 | }
7 |
8 | input {
9 | font-size: 1.5em;
10 | padding: .4em .6em;
11 | }
12 |
13 | .result h2 {
14 | margin-top: 2em;
15 | }
16 |
--------------------------------------------------------------------------------
/examples/button.nim:
--------------------------------------------------------------------------------
1 | include karax / prelude
2 |
3 | var lines: seq[kstring] = @[]
4 |
5 | proc createDom(): VNode =
6 | result = buildHtml(tdiv):
7 | button:
8 | text "Say hello!"
9 | proc onclick(ev: Event; n: VNode) =
10 | lines.add "Hello simulated universe"
11 | for x in lines:
12 | tdiv:
13 | text x
14 |
15 | setRenderer createDom
16 |
--------------------------------------------------------------------------------
/examples/buttonlambda.nim:
--------------------------------------------------------------------------------
1 | include karax / prelude
2 | from sugar import `=>`
3 |
4 | var lines: seq[kstring] = @[]
5 |
6 | proc createDom(): VNode =
7 | result = buildHtml(tdiv):
8 | button(onclick = () => lines.add "Hello simulated universe"):
9 | text "Say hello!"
10 | for x in lines:
11 | tdiv:
12 | text x
13 |
14 | setRenderer createDom
15 |
--------------------------------------------------------------------------------
/examples/carousel/carousel.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Carousel app
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/carousel/carousel.nim:
--------------------------------------------------------------------------------
1 | ## This demo shows how you can develop your own stateful components with Karax.
2 |
3 | import vdom, vstyles, karax, karaxdsl, jdict, jstrutils, kdom
4 |
5 | type
6 | Carousel = ref object of VComponent
7 | counter: int
8 | cntdown: int
9 | timer: TimeOut
10 | list: seq[cstring]
11 |
12 | const ticksUntilChange = 5
13 |
14 | var
15 | images: seq[cstring] = @[cstring"a", "b", "c", "d"]
16 |
17 | var
18 | refA:Carousel
19 | refB:Carousel
20 | refC:Carousel
21 | refD:Carousel
22 | proc render(x: VComponent): VNode =
23 | let self = Carousel(x)
24 |
25 | proc docount() =
26 | dec self.cntdown
27 | if self.cntdown == 0:
28 | self.counter = (self.counter + 1) mod self.list.len
29 | self.cntdown = ticksUntilChange
30 | else:
31 | self.timer = setTimeout(docount, 30)
32 | markDirty(self)
33 | redraw()
34 |
35 | proc onclick(ev: Event; n: VNode) =
36 | if self.timer != nil:
37 | clearTimeout(self.timer)
38 | self.timer = setTimeout(docount, 30)
39 |
40 | let col =
41 | case self.counter
42 | of 0: cstring"#4d4d4d"
43 | of 1: cstring"#ff00ff"
44 | of 2: cstring"#00ffff"
45 | of 3: cstring"#ffff00"
46 | else: cstring"red"
47 | result = buildHtml(tdiv()):
48 | text self.list[self.counter]
49 | button(onclick = onclick):
50 | text "Next"
51 | tdiv(style = style(StyleAttr.color, col)):
52 | text "This changes its color."
53 | if self.cntdown != ticksUntilChange:
54 | text &self.cntdown
55 |
56 | proc carousel(nref:var Carousel): Carousel =
57 | if nref == nil:
58 | nref = newComponent(Carousel, render)
59 | nref.list = images
60 | nref.cntdown = ticksUntilChange
61 | return nref
62 | else:
63 | return nref
64 |
65 | proc createDom(): VNode =
66 | result = buildHtml(table):
67 | tr:
68 | td:
69 | carousel(nref=refA)
70 | td:
71 | carousel(nref=refB)
72 | tr:
73 | td:
74 | carousel(nref=refC)
75 | td:
76 | carousel(nref=refD)
77 |
78 | setRenderer createDom
79 |
--------------------------------------------------------------------------------
/examples/carousel/carousel.nims:
--------------------------------------------------------------------------------
1 | --path: "../../karax"
2 |
--------------------------------------------------------------------------------
/examples/config.nims:
--------------------------------------------------------------------------------
1 | --path: ".."
2 |
--------------------------------------------------------------------------------
/examples/hellostyle.nim:
--------------------------------------------------------------------------------
1 | include karax / prelude
2 | import karax / vstyles
3 |
4 | proc createDom(): VNode =
5 | result = buildHtml(tdiv):
6 | tdiv(style = style(StyleAttr.color, "red".cstring)):
7 | text "red"
8 | tdiv(style = style(StyleAttr.color, "blue")):
9 | text "blue"
10 | # explicit `kstring` required for varargs overload
11 | tdiv(style = style((fontStyle, "italic".kstring), (color, "orange".kstring))):
12 | text "italic orange"
13 | # can use a string directly
14 | tdiv(style = "font-style: oblique; color: pink".toCss):
15 | text "oblique pink"
16 |
17 | setRenderer createDom
18 |
--------------------------------------------------------------------------------
/examples/hellouniverse.nim:
--------------------------------------------------------------------------------
1 | include karax / prelude
2 | import random
3 |
4 | proc createDom(): VNode =
5 | result = buildHtml(tdiv):
6 | if rand(100) <= 50:
7 | text "Hello World!"
8 | else:
9 | text "Hello Universe"
10 |
11 | randomize()
12 | setRenderer createDom
13 |
--------------------------------------------------------------------------------
/examples/helloworld.nim:
--------------------------------------------------------------------------------
1 | include karax / prelude
2 |
3 | proc createDom(): VNode =
4 | result = buildHtml(tdiv):
5 | text "Hello World!"
6 |
7 | setRenderer createDom
8 |
--------------------------------------------------------------------------------
/examples/karaxtmulti/README.md:
--------------------------------------------------------------------------------
1 | Example to use multiple karax instances on the same page.
2 |
3 | build example:
4 |
5 | ```
6 | nim js -d:kxiname="app1" .\app1.nim
7 | nim js -d:kxiname="app2" .\app2.nim
8 | ```
9 |
10 | then start `index.html` in the browser.
--------------------------------------------------------------------------------
/examples/karaxtmulti/app1.nim:
--------------------------------------------------------------------------------
1 | include karax / prelude
2 |
3 | var lines: seq[kstring] = @[]
4 |
5 | proc createDom(): VNode =
6 | result = buildHtml(tdiv):
7 | button:
8 | text "Say hello!"
9 | proc onclick(ev: Event; n: VNode) =
10 | lines.add "Hello simulated universe"
11 | for x in lines:
12 | tdiv:
13 | text x
14 |
15 | var a1 = setRenderer(createDom, "ROOT1")
--------------------------------------------------------------------------------
/examples/karaxtmulti/app2.nim:
--------------------------------------------------------------------------------
1 | include karax / prelude
2 | import karax / kdom
3 |
4 | let id = "foo1"
5 | let id2 = "foo2"
6 | let valueInitial = "150"
7 | var value = valueInitial
8 | proc updateText() =
9 | value = $document.getElementById($id).value
10 | proc createDom(): VNode =
11 | result = buildHtml(tdiv):
12 | input(`type`="range", min = "10", max = "170", value = valueInitial, id = id):
13 | proc oninput(ev: Event; target: VNode) = updateText()
14 | tdiv(id=id2):
15 | text value
16 |
17 |
18 | var a2 = setRenderer(createDom, "ROOT2")
--------------------------------------------------------------------------------
/examples/karaxtmulti/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Multiple Karax apps
6 |
7 |
8 |
9 |
10 |
15 |
16 |
Use multiple karax apps.
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
Some example html
26 |
27 |
28 |
Company
29 |
Contact
30 |
Country
31 |
32 |
33 |
Alfreds Futterkiste
34 |
Maria Anders
35 |
Germany
36 |
37 |
38 |
Centro comercial Moctezuma
39 |
Francisco Chang
40 |
Mexico
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/examples/login.nim:
--------------------------------------------------------------------------------
1 | include karax / prelude
2 | from sugar import `=>`
3 | import karax / errors
4 |
5 | proc loginField(desc, field, class: kstring;
6 | validator: proc (field: kstring): proc ()): VNode =
7 | result = buildHtml(tdiv):
8 | label(`for` = field):
9 | text desc
10 | input(class = class, id = field, onkeyup = validator(field))
11 |
12 | # some consts in order to prevent typos:
13 | const
14 | username = kstring"username"
15 | password = kstring"password"
16 |
17 | proc validateNotEmpty(field: kstring): proc () =
18 | result = proc () =
19 | let x = getVNodeById(field)
20 | if x.getInputText == "":
21 | errors.setError(field, field & " must not be empty")
22 | else:
23 | errors.setError(field, "")
24 |
25 | var loggedIn: bool
26 |
27 | proc loginDialog(): VNode =
28 | result = buildHtml(tdiv):
29 | if not loggedIn:
30 | loginField("Name: ", username, "input", validateNotEmpty)
31 | loginField("Password: ", password, "password", validateNotEmpty)
32 | button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()):
33 | text "Login"
34 | p:
35 | text errors.getError(username)
36 | p:
37 | text errors.getError(password)
38 | else:
39 | p:
40 | text "You are now logged in."
41 |
42 | setError username, username & " must not be empty"
43 | setError password, password & " must not be empty"
44 |
45 | when not declared(toychat):
46 | setRenderer loginDialog
47 |
--------------------------------------------------------------------------------
/examples/mediaplayer/mediaplayer.nim:
--------------------------------------------------------------------------------
1 |
2 | import karax / [karax, karaxdsl, vdom, kdom, compact]
3 |
4 | const
5 | Play = 0
6 | Big = 1
7 | Small = 2
8 | Normal = 3
9 |
10 | proc paused(n: Node): bool {.importcpp: "#.paused".}
11 | proc play(n: Node) {.importcpp.}
12 | proc pause(n: Node) {.importcpp.}
13 | proc `width=`(n: Node, w: int) {.importcpp: "#.width = #".}
14 |
15 | proc mplayer*(id, resource: cstring): VNode {.compact.} =
16 | proc handler(ev: Event; n: VNode) =
17 | let myVideo = document.getElementById(id)
18 | case n.index
19 | of Play:
20 | if myVideo.paused:
21 | myVideo.play()
22 | else:
23 | myVideo.pause()
24 | of Big: myVideo.width = 560
25 | of Small: myVideo.width = 320
26 | of Normal: myVideo.width = 420
27 | else: discard
28 |
29 | result = buildHtml(tdiv):
30 | button(onclick=handler, index=Play):
31 | text "Play/Pause"
32 | button(onclick=handler, index=Big):
33 | text "Big"
34 | button(onclick=handler, index=Small):
35 | text "Small"
36 | button(onclick=handler, index=Normal):
37 | text "Normal"
38 | br()
39 | br()
40 | video(id=id, width="420"):
41 | source(src=resource, `type`="video/mp4"):
42 | text "Your browser does not support HTML5 video."
43 |
--------------------------------------------------------------------------------
/examples/mediaplayer/playerapp.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Player app
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/mediaplayer/playerapp.nim:
--------------------------------------------------------------------------------
1 | ## Example app that shows how to write and embed a custom component.
2 |
3 | include karax/prelude
4 | import mediaplayer
5 |
6 | const url = "https://www.w3schools.com/html/mov_bbb.mp4"
7 |
8 | proc createDom(): VNode =
9 | result = buildHtml(table):
10 | tr:
11 | td:
12 | mplayer("vid1", url)
13 | td:
14 | mplayer("vid2", url)
15 | tr:
16 | td:
17 | mplayer("vid3", url)
18 | td:
19 | mplayer("vid4", url)
20 |
21 | setRenderer createDom
22 |
--------------------------------------------------------------------------------
/examples/mediaplayer/playerapp.nims:
--------------------------------------------------------------------------------
1 | --path: "../.."
2 |
--------------------------------------------------------------------------------
/examples/scrollapp/scrollapp.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Todo app
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/scrollapp/scrollapp.nim:
--------------------------------------------------------------------------------
1 | ## Example that shows how to accomplish an "infinitely scrolling" app.
2 |
3 | include karax/prelude
4 | import karax / [jstrutils, kdom, vstyles]
5 |
6 | var entries: seq[cstring] = @[]
7 | for i in 1..500:
8 | entries.add(cstring("Entry ") & &i)
9 |
10 | proc scrollEvent(ev: Event; n: VNode) =
11 | let d = n.dom
12 | if d != nil and inViewport(d.lastChild):
13 | # "load" more data:
14 | for i in 1..50:
15 | entries.add(cstring("Loaded Entry ") & &i)
16 |
17 | proc createDom(): VNode =
18 | result = buildHtml():
19 | tdiv(onscroll=scrollEvent, style=style(
20 | (StyleAttr.height, cstring"400px"), (StyleAttr.overflow, cstring"scroll"))):
21 | for x in entries:
22 | tdiv:
23 | text x
24 |
25 | setRenderer createDom
26 |
--------------------------------------------------------------------------------
/examples/scrollapp/scrollapp.nims:
--------------------------------------------------------------------------------
1 | --path: "../.."
2 |
--------------------------------------------------------------------------------
/examples/slider.nim:
--------------------------------------------------------------------------------
1 | include karax / prelude
2 | import karax / kdom
3 |
4 | let id = "foo1"
5 | let id2 = "foo2"
6 | let valueInitial = "150"
7 | var value = valueInitial
8 | proc updateText() =
9 | value = $document.getElementById($id).value
10 | proc createDom(): VNode =
11 | result = buildHtml(tdiv):
12 | input(`type`="range", min = "10", max = "170", value = valueInitial, id = id):
13 | proc oninput(ev: Event; target: VNode) = updateText()
14 | tdiv(id=id2):
15 | text value
16 |
17 |
18 | setRenderer createDom, "ROOT"
--------------------------------------------------------------------------------
/examples/todoapp/style.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | .todomvc-wrapper {
8 | visibility: visible !important;
9 | }
10 |
11 | button {
12 | margin: 0;
13 | padding: 0;
14 | border: 0;
15 | background: none;
16 | font-size: 100%;
17 | vertical-align: baseline;
18 | font-family: inherit;
19 | font-weight: inherit;
20 | color: inherit;
21 | -webkit-appearance: none;
22 | appearance: none;
23 | -webkit-font-smoothing: antialiased;
24 | -moz-font-smoothing: antialiased;
25 | font-smoothing: antialiased;
26 | }
27 |
28 | body {
29 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
30 | line-height: 1.4em;
31 | background: #f5f5f5;
32 | color: #4d4d4d;
33 | min-width: 230px;
34 | max-width: 550px;
35 | margin: 0 auto;
36 | -webkit-font-smoothing: antialiased;
37 | -moz-font-smoothing: antialiased;
38 | font-smoothing: antialiased;
39 | font-weight: 300;
40 | }
41 |
42 | button,
43 | input[type="checkbox"] {
44 | outline: none;
45 | }
46 |
47 | .hidden {
48 | display: none;
49 | }
50 |
51 | .todoapp {
52 | background: #fff;
53 | margin: 130px 0 40px 0;
54 | position: relative;
55 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
56 | 0 25px 50px 0 rgba(0, 0, 0, 0.1);
57 | }
58 |
59 | .todoapp input::-webkit-input-placeholder {
60 | font-style: italic;
61 | font-weight: 300;
62 | color: #e6e6e6;
63 | }
64 |
65 | .todoapp input::-moz-placeholder {
66 | font-style: italic;
67 | font-weight: 300;
68 | color: #e6e6e6;
69 | }
70 |
71 | .todoapp input::input-placeholder {
72 | font-style: italic;
73 | font-weight: 300;
74 | color: #e6e6e6;
75 | }
76 |
77 | .todoapp h1 {
78 | position: absolute;
79 | top: -155px;
80 | width: 100%;
81 | font-size: 100px;
82 | font-weight: 100;
83 | text-align: center;
84 | color: rgba(175, 47, 47, 0.15);
85 | -webkit-text-rendering: optimizeLegibility;
86 | -moz-text-rendering: optimizeLegibility;
87 | text-rendering: optimizeLegibility;
88 | }
89 |
90 | .new-todo,
91 | .edit {
92 | position: relative;
93 | margin: 0;
94 | width: 100%;
95 | font-size: 24px;
96 | font-family: inherit;
97 | font-weight: inherit;
98 | line-height: 1.4em;
99 | border: 0;
100 | outline: none;
101 | color: inherit;
102 | padding: 6px;
103 | border: 1px solid #999;
104 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
105 | box-sizing: border-box;
106 | -webkit-font-smoothing: antialiased;
107 | -moz-font-smoothing: antialiased;
108 | font-smoothing: antialiased;
109 | }
110 |
111 | .new-todo {
112 | padding: 16px 16px 16px 60px;
113 | border: none;
114 | background: rgba(0, 0, 0, 0.003);
115 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
116 | }
117 |
118 | .main {
119 | position: relative;
120 | z-index: 2;
121 | border-top: 1px solid #e6e6e6;
122 | }
123 |
124 | label[for='toggle-all'] {
125 | display: none;
126 | }
127 |
128 | .toggle-all {
129 | position: absolute;
130 | top: -55px;
131 | left: -12px;
132 | width: 60px;
133 | height: 34px;
134 | text-align: center;
135 | border: none; /* Mobile Safari */
136 | }
137 |
138 | .toggle-all:before {
139 | content: '>';
140 | font-size: 22px;
141 | color: #e6e6e6;
142 | padding: 10px 27px 10px 27px;
143 | }
144 |
145 | .toggle-all:checked:before {
146 | color: #737373;
147 | }
148 |
149 | .todo-list {
150 | margin: 0;
151 | padding: 0;
152 | list-style: none;
153 | }
154 |
155 | .todo-list li {
156 | position: relative;
157 | font-size: 24px;
158 | border-bottom: 1px solid #ededed;
159 | }
160 |
161 | .todo-list li:last-child {
162 | border-bottom: none;
163 | }
164 |
165 | .todo-list li.editing {
166 | border-bottom: none;
167 | padding: 0;
168 | }
169 |
170 | .todo-list li.editing .edit {
171 | display: block;
172 | width: 506px;
173 | padding: 13px 17px 12px 17px;
174 | margin: 0 0 0 43px;
175 | }
176 |
177 | .todo-list li.editing .view {
178 | display: none;
179 | }
180 |
181 | .todo-list li .toggle {
182 | text-align: center;
183 | width: 40px;
184 | /* auto, since non-WebKit browsers doesn't support input styling */
185 | height: auto;
186 | position: absolute;
187 | top: 0;
188 | bottom: 0;
189 | margin: auto 0;
190 | border: none; /* Mobile Safari */
191 | -webkit-appearance: none;
192 | appearance: none;
193 | }
194 |
195 | .todo-list li .toggle:after {
196 | content: url('data:image/svg+xml,');
197 | }
198 |
199 | .todo-list li .toggle:checked:after {
200 | content: url('data:image/svg+xml,');
201 | }
202 |
203 | .todo-list li label {
204 | white-space: pre-line;
205 | word-break: break-all;
206 | padding: 15px 60px 15px 15px;
207 | margin-left: 45px;
208 | display: block;
209 | line-height: 1.2;
210 | transition: color 0.4s;
211 | }
212 |
213 | .todo-list li.completed label {
214 | color: #d9d9d9;
215 | text-decoration: line-through;
216 | }
217 |
218 | .todo-list li .destroy {
219 | display: none;
220 | position: absolute;
221 | top: 0;
222 | right: 10px;
223 | bottom: 0;
224 | width: 40px;
225 | height: 40px;
226 | margin: auto 0;
227 | font-size: 30px;
228 | color: #cc9a9a;
229 | margin-bottom: 11px;
230 | transition: color 0.2s ease-out;
231 | }
232 |
233 | .todo-list li .destroy:hover {
234 | color: #af5b5e;
235 | }
236 |
237 | .todo-list li .destroy:after {
238 | content: 'x';
239 | }
240 |
241 | .todo-list li:hover .destroy {
242 | display: block;
243 | }
244 |
245 | .todo-list li.editing:last-child {
246 | margin-bottom: -1px;
247 | }
248 |
249 | .footer {
250 | color: #777;
251 | padding: 10px 15px;
252 | height: 20px;
253 | text-align: center;
254 | border-top: 1px solid #e6e6e6;
255 | }
256 |
257 | .footer:before {
258 | content: '';
259 | position: absolute;
260 | right: 0;
261 | bottom: 0;
262 | left: 0;
263 | height: 50px;
264 | overflow: hidden;
265 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
266 | 0 8px 0 -3px #f6f6f6,
267 | 0 9px 1px -3px rgba(0, 0, 0, 0.2),
268 | 0 16px 0 -6px #f6f6f6,
269 | 0 17px 2px -6px rgba(0, 0, 0, 0.2);
270 | }
271 |
272 | .todo-count {
273 | float: left;
274 | text-align: left;
275 | }
276 |
277 | .todo-count strong {
278 | font-weight: 300;
279 | }
280 |
281 | .filters {
282 | margin: 0;
283 | padding: 0;
284 | list-style: none;
285 | position: absolute;
286 | right: 0;
287 | left: 0;
288 | }
289 |
290 | .filters li {
291 | display: inline;
292 | }
293 |
294 | .filters li a {
295 | color: inherit;
296 | margin: 3px;
297 | padding: 3px 7px;
298 | text-decoration: none;
299 | border: 1px solid transparent;
300 | border-radius: 3px;
301 | }
302 |
303 | .filters li a.selected,
304 | .filters li a:hover {
305 | border-color: rgba(175, 47, 47, 0.1);
306 | }
307 |
308 | .filters li a.selected {
309 | border-color: rgba(175, 47, 47, 0.2);
310 | }
311 |
312 | .clear-completed,
313 | html .clear-completed:active {
314 | float: right;
315 | position: relative;
316 | line-height: 20px;
317 | text-decoration: none;
318 | cursor: pointer;
319 | position: relative;
320 | }
321 |
322 | .clear-completed:hover {
323 | text-decoration: underline;
324 | }
325 |
326 | .info {
327 | margin: 65px auto 0;
328 | color: #bfbfbf;
329 | /* font-size: 10px; */
330 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
331 | text-align: center;
332 | }
333 |
334 | .info p {
335 | line-height: 1;
336 | }
337 |
338 | .info a {
339 | color: inherit;
340 | text-decoration: none;
341 | font-weight: 400;
342 | }
343 |
344 | .info a:hover {
345 | text-decoration: underline;
346 | }
347 |
348 | /*
349 | Hack to remove background from Mobile Safari.
350 | Can't use it globally since it destroys checkboxes in Firefox
351 | */
352 | @media screen and (-webkit-min-device-pixel-ratio:0) {
353 | .toggle-all,
354 | .todo-list li .toggle {
355 | background: none;
356 | }
357 |
358 | .todo-list li .toggle {
359 | height: 40px;
360 | }
361 |
362 | .toggle-all {
363 | -webkit-transform: rotate(90deg);
364 | transform: rotate(90deg);
365 | -webkit-appearance: none;
366 | appearance: none;
367 | }
368 | }
369 |
370 | @media (max-width: 430px) {
371 | .footer {
372 | height: 50px;
373 | }
374 |
375 | .filters {
376 | bottom: 10px;
377 | }
378 | }
--------------------------------------------------------------------------------
/examples/todoapp/todoapp.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Todo app
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/examples/todoapp/todoapp.nim:
--------------------------------------------------------------------------------
1 |
2 | import karax / [vdom, karax, karaxdsl, jstrutils, compact, localstorage]
3 |
4 | type
5 | Filter = enum
6 | all, active, completed
7 |
8 | var
9 | selectedEntry = -1
10 | filter: Filter
11 | entriesLen: int
12 | doneswitch = true
13 | const
14 | contentSuffix = cstring"content"
15 | completedSuffix = cstring"completed"
16 | lenSuffix = cstring"entriesLen"
17 |
18 | proc getEntryContent(pos: int): cstring =
19 | result = getItem(&pos & contentSuffix)
20 | if result == cstring"null":
21 | result = nil
22 |
23 | proc isCompleted(pos: int): bool =
24 | var value = getItem(&pos & completedSuffix)
25 | result = value == cstring"true"
26 |
27 | proc setEntryContent(pos: int, content: cstring) =
28 | setItem(&pos & contentSuffix, content)
29 |
30 | proc markAsCompleted(pos: int, completed: bool) =
31 | setItem(&pos & completedSuffix, &completed)
32 |
33 | proc addEntry(content: cstring, completed: bool) =
34 | setEntryContent(entriesLen, content)
35 | markAsCompleted(entriesLen, completed)
36 | inc entriesLen
37 | setItem(lenSuffix, &entriesLen)
38 |
39 | proc updateEntry(pos: int, content: cstring, completed: bool) =
40 | setEntryContent(pos, content)
41 | markAsCompleted(pos, completed)
42 |
43 | proc onTodoEnter(ev: Event; n: VNode) =
44 | if n.value.strip() != "":
45 | addEntry(n.value, false)
46 | n.value = ""
47 |
48 | proc removeHandler(ev: Event; n: VNode) =
49 | updateEntry(n.index, cstring(nil), false)
50 |
51 | proc editHandler(ev: Event; n: VNode) =
52 | selectedEntry = n.index
53 |
54 | proc focusLost(ev: Event; n: VNode) = selectedEntry = -1
55 |
56 | proc editEntry(ev: Event; n: VNode) =
57 | setEntryContent(n.index, n.value)
58 | selectedEntry = -1
59 |
60 | proc toggleEntry(ev: Event; n: VNode) =
61 | let id = n.index
62 | markAsCompleted(id, not isCompleted(id))
63 |
64 | proc onAllDone(ev: Event; n: VNode) =
65 | for i in 0..
2 |
3 |
4 |
5 | Karax AJAX Test
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/experiments/ajaxtest.nim:
--------------------------------------------------------------------------------
1 | import kdom, kajax
2 |
3 | proc cb(httpStatus: int, response: cstring) =
4 | echo "Worked!"
5 |
6 | ajaxGet("https://httpbin.org/get", @[], cb)
7 |
--------------------------------------------------------------------------------
/experiments/config.nims:
--------------------------------------------------------------------------------
1 | --path: "../karax"
2 | --path: ".."
3 |
--------------------------------------------------------------------------------
/experiments/dyn.nim:
--------------------------------------------------------------------------------
1 |
2 |
3 | type
4 | EntryPoint = proc()
5 |
6 |
7 | proc dynmain =
8 | echo "dynamically loaded"
9 |
10 |
11 | var plugins {.importc.}: seq[(string, EntryPoint)]
12 |
13 | plugins.add(("dyn", EntryPoint dynmain))
14 |
--------------------------------------------------------------------------------
/experiments/echartstest.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Karax ECharts Test
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/experiments/echartstest.nim:
--------------------------------------------------------------------------------
1 | import karax / [kbase, vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, jjson]
2 |
3 | type
4 | Views = enum
5 | Customers, Products
6 |
7 | var
8 | currentView = Customers
9 |
10 | type
11 | EChart* = ref object
12 |
13 | proc echartsInit(n: Element): EChart {.importc: "echarts.init".}
14 | proc setOption(x: EChart; option: JsonNode) {.importcpp.}
15 |
16 | proc postRender(data: RouterData) =
17 | if currentView == Products:
18 | let myChart = echartsInit(kdom.getElementById("echartSection"))
19 | # specify chart configuration item and data
20 | let option = %*{
21 | "title": {
22 | "text": "ECharts entry example"
23 | },
24 | "tooltip": {},
25 | "legend": {
26 | "data": ["Sales"]
27 | },
28 | "xAxis": {
29 | "data": ["shirt","cardign","chiffon shirt","pants","heels","socks"]
30 | },
31 | "yAxis": {},
32 | "series": [{
33 | "name": "Sales",
34 | "type": "bar",
35 | "data": [5, 20, 36, 10, 10, 20]
36 | }]
37 | }
38 | myChart.setOption(option)
39 |
40 | proc createDom(data: RouterData): VNode =
41 | let hash = data.hashPart
42 | if hash == cstring"#/Products": currentView = Products
43 | else: currentView = Customers
44 |
45 | result = buildHtml(tdiv):
46 | ul(class = "tabs"):
47 | for v in low(Views)..high(Views):
48 | li:
49 | a(href = "#/" & $v):
50 | text cstring($v)
51 |
52 | if currentView == Products:
53 | tdiv(id = "echartSection", style = style((width, kstring"600px"), (height, kstring"400px")))
54 | tdiv:
55 | text "other section"
56 |
57 | setRenderer createDom, "ROOT", postRender
58 | setForeignNodeId "echartSection"
59 |
--------------------------------------------------------------------------------
/experiments/menus.nim:
--------------------------------------------------------------------------------
1 | ## Example program that shows how to create menus with Karax.
2 |
3 | include prelude
4 | import jstrutils, kdom
5 |
6 | proc contentA(): VNode =
7 | result = buildHtml(tdiv):
8 | text "content A"
9 |
10 | proc contentB(): VNode =
11 | result = buildHtml(tdiv):
12 | text "content B"
13 |
14 | proc contentC(): VNode =
15 | result = buildHtml(tdiv):
16 | text "content C"
17 |
18 |
19 |
20 | type
21 | MenuItemHandler = proc(): VNode
22 |
23 | var content: MenuItemHandler = contentA
24 |
25 | proc menuAction(x: MenuItemHandler): proc() =
26 | result = proc() = content = x
27 |
28 | proc buildMenu(): VNode =
29 | result = buildHtml(tdiv):
30 | nav(class="navbar is-primary"):
31 | tdiv(class="navbar-brand"):
32 | a(class="navbar-item", onclick = menuAction(contentA)):
33 | strong:
34 | text "My awesome menu"
35 |
36 | tdiv(id="navMenuTransparentExample", class="navbar-menu"):
37 | tdiv(class = "navbar-start"):
38 | tdiv(class="navbar-item has-dropdown is-hoverable"):
39 | a(class="navbar-link", onclick = menuAction(contentB)):
40 | text "Masters"
41 | tdiv(class="navbar-dropdown is-boxed"):
42 | a(class="navbar-item", onclick = menuAction(contentC)):
43 | text "Inventory"
44 | a(class="navbar-item", onclick = menuAction(contentC)):
45 | text "Product"
46 | hr(class="navbar-divider"):
47 | a(class="navbar-item", onclick = menuAction(contentC)):
48 | text "Product"
49 |
50 | tdiv(class="navbar-item has-dropdown is-hoverable"):
51 | a(class="navbar-link", onclick = menuAction(contentC)):
52 | text "Transactions"
53 | tdiv(class = "navbar-dropdown is-boxed", id="blogDropdown"):
54 | a(class="navbar-item", onclick = menuAction(contentC)):
55 | text "Purchase Bill"
56 | a(class="navbar-item", onclick = menuAction(contentC)):
57 | text "Purchase Return"
58 | a(class="navbar-item", onclick = menuAction(contentC)):
59 | text "Sale Bill"
60 | hr(class="navbar-divider"):
61 | a(class="navbar-item", onclick = menuAction(contentC)):
62 | text "Stock Adjustment"
63 | content()
64 |
65 | setRenderer buildMenu
66 |
--------------------------------------------------------------------------------
/experiments/nativenodes.nim:
--------------------------------------------------------------------------------
1 |
2 |
3 | include karax / prelude
4 | import karax / kdom
5 |
6 | proc myInput: VNode =
7 | result = buildHtml:
8 | input()
9 |
10 | var inp = vnodeToDom(myInput())
11 |
12 | proc myAwesomeComponent(x: Node): VNode =
13 | result = dthunk(x)
14 |
15 | proc main: VNode =
16 | result = myAwesomeComponent(inp)
17 |
18 | setRenderer main
19 |
--------------------------------------------------------------------------------
/experiments/nextgen.nim:
--------------------------------------------------------------------------------
1 |
2 | import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, reactive
3 |
4 | proc textInput*(text: RString; focus: RBool): VNode {.track.} =
5 | proc onFlip(ev: Event; target: VNode) =
6 | focus <- not focus.value
7 |
8 | proc onKeyupEnter(ev: Event; target: VNode) =
9 | text <- target.value
10 |
11 | proc onkeyup(ev: Event; n: VNode) =
12 | # keep displayValue up to date, but do not tell the client yet!
13 | text.value = n.value
14 |
15 | result = buildHtml(input(`type`="text",
16 | value=text.value, onblur=onFlip, onfocus=onFlip,
17 | onkeyupenter=onkeyupenter, onkeyup=onkeyup, setFocus=focus.value))
18 |
19 | var
20 | errmsg = rstr("")
21 |
22 | makeReactive:
23 | type
24 | User = ref object
25 | firstname, lastname: cstring
26 | selected: bool
27 |
28 | var gu = newRSeq(@[ (User(rawFirstname: "Some", rawLastname: "Body")),
29 | (User(rawFirstname: "Some", rawLastname: "One")),
30 | (User(rawFirstname: "Some", rawLastname: "Two"))])
31 | var prevSelected: User = nil #newReactive[User](nil)
32 |
33 | proc unselect() =
34 | if prevSelected != nil:
35 | prevSelected.selected = false
36 | prevSelected = nil
37 |
38 | proc select(u: User) =
39 | unselect()
40 | u.selected = true
41 | prevSelected = u
42 |
43 | proc toUI*(isFirstname: bool): RString =
44 | result = RString()
45 | result.subscribe proc (v: cstring) =
46 | if v.len > 0:
47 | let p = prevSelected #selected.value
48 | if p != nil:
49 | if isFirstName:
50 | p.firstname = v
51 | else:
52 | p.lastname = v
53 | unselect()
54 | errmsg <- ""
55 | else:
56 | errmsg <- "name must not be empty"
57 |
58 | var inpFirstname = toUI(true)
59 | var inpLastname = toUI(false)
60 |
61 | proc adaptFocus(def = false): RBool =
62 | result = RBool()
63 | result.value = def
64 | when false:
65 | result.subscribe proc (hasFocus: bool) =
66 | if not hasFocus:
67 | unselect()
68 |
69 | var focusA = adaptFocus()
70 | var focusB = adaptFocus()
71 |
72 | proc styler(): VStyle =
73 | result = style(
74 | (StyleAttr.position, cstring"relative"),
75 | (StyleAttr.paddingLeft, cstring"10px"),
76 | (StyleAttr.paddingRight, cstring"5px"),
77 | (StyleAttr.height, cstring"30px"),
78 | (StyleAttr.lineHeight, cstring"30px"),
79 | (StyleAttr.border, cstring"solid 8px " & (if focusA.value: cstring"red" else: cstring"black")),
80 | (StyleAttr.fontSize, cstring"12px"),
81 | (StyleAttr.fontWeight, cstring"600")
82 | )
83 |
84 | var clicks = 0
85 |
86 | proc renderUser(u: User): VNode {.track.} =
87 | result = buildHtml(tdiv):
88 | if u.selected:
89 | inpFirstname <- u.firstname
90 | tdiv:
91 | textInput inpFirstname, focusA
92 | inpLastname <- u.lastname
93 | tdiv:
94 | textInput inpLastname, focusB
95 | else:
96 | button:
97 | text "..."
98 | proc onclick(ev: Event; n: VNode) =
99 | select(u)
100 | text u.firstname & " " & u.lastname
101 | button:
102 | text "(x)"
103 | proc onclick(ev: Event; n: VNode) =
104 | gu.deleteElem(u)
105 |
106 | proc main(gu: RSeq[User]): VNode =
107 | result = buildHtml(tdiv):
108 | tdiv:
109 | button:
110 | text "Add User"
111 | proc onclick(ev: Event; n: VNode) =
112 | inc clicks
113 | gu.add User(rawFirstname: "Added", rawLastname: &clicks)
114 | tdiv:
115 | text errmsg
116 | vmapIt(gu, tdiv, renderUser(it))
117 |
118 | proc init(): VNode = main(gu)
119 |
120 | setInitializer(init)
121 |
--------------------------------------------------------------------------------
/experiments/oldwidgets.nim:
--------------------------------------------------------------------------------
1 | var
2 | linkCounter: int
3 |
4 | proc link*(id: int): VNode =
5 | result = newVNode(VNodeKind.anchor)
6 | result.setAttr("href", "#")
7 | inc linkCounter
8 | result.setAttr("id", $linkCounter & ":" & $id)
9 |
10 | proc link*(action: EventHandler): VNode =
11 | result = newVNode(VNodeKind.anchor)
12 | result.setAttr("href", "#")
13 | result.setOnclick action
14 |
15 | when false:
16 | proc button*(caption: cstring; action: EventHandler; disabled=false): VNode =
17 | result = newVNode(VNodeKind.button)
18 | result.add text(caption)
19 | if action != nil:
20 | result.setOnClick action
21 | if disabled:
22 | result.setAttr("disabled", "true")
23 |
24 | proc select*(choices: openarray[cstring]): VNode =
25 | result = newVNode(VNodeKind.select)
26 | var i = 0
27 | for c in choices:
28 | result.add tree(VNodeKind.option, [(cstring"value", toCstr(i))], text(c))
29 | inc i
30 |
31 | proc select*(choices: openarray[(int, cstring)]): VNode =
32 | result = newVNode(VNodeKind.select)
33 | for c in choices:
34 | result.add tree(VNodeKind.option, [(cstring"value", toCstr(c[0]))], text(c[1]))
35 |
36 | var radioCounter: int
37 |
38 | proc radio*(choices: openarray[(int, cstring)]): VNode =
39 | result = newVNode(VNodeKind.fieldset)
40 | var i = 0
41 | inc radioCounter
42 | for c in choices:
43 | let id = cstring"radio_" & c[1] & toCstr(i)
44 | var kid = tree(VNodeKind.input, [(cstring"type", cstring"radio"),
45 | (cstring"id", id), (cstring"name", cstring"radio" & toCStr(radioCounter)),
46 | (cstring"value", toCStr(c[0]))])
47 | if i == 0:
48 | kid.setAttr(cstring"checked", cstring"checked")
49 | var lab = tree(VNodeKind.label, [(cstring"for", id)], text(c[1]))
50 | kid.add lab
51 | result.add kid
52 | inc i
53 |
54 | proc tag*(kind: VNodeKind; id=cstring(nil), class=cstring(nil)): VNode =
55 | result = newVNode(kind)
56 | result.id = id
57 | result.class = class
58 |
59 | proc tdiv*(id=cstring(nil), class=cstring(nil)): VNode = tag(VNodeKind.tdiv, id, class)
60 | proc span*(id=cstring(nil), class=cstring(nil)): VNode = tag(VNodeKind.span, id, class)
61 |
62 | proc valueAsInt*(e: Node): int = parseInt(e.value)
63 |
64 | proc th*(s: cstring): VNode =
65 | result = newVNode(VNodeKind.th)
66 | result.add text(s)
67 |
68 | proc td*(s: string): VNode =
69 | result = newVNode(VNodeKind.td)
70 | result.add text(s)
71 |
72 | proc td*(s: VNode): VNode =
73 | result = newVNode(VNodeKind.td)
74 | result.add s
75 |
76 | proc td*(class: cstring; s: VNode): VNode =
77 | result = newVNode(VNodeKind.td)
78 | result.add s
79 | result.class = class
80 |
81 | proc table*(class=cstring(nil), kids: varargs[VNode]): VNode =
82 | result = tag(VNodeKind.table, nil, class)
83 | for k in kids: result.add k
84 |
85 | proc tr*(kids: varargs[VNode]): VNode =
86 | result = newVNode(VNodeKind.tr)
87 | for k in kids:
88 | if k.kind in {VNodeKind.td, VNodeKind.th}:
89 | result.add k
90 | else:
91 | result.add td(k)
92 |
93 | proc suffix*(s, prefix: cstring): cstring =
94 | if s.startsWith(prefix):
95 | result = s.substr(prefix.len)
96 | else:
97 | kout(cstring"bug! " & s & cstring" does not start with " & prefix)
98 |
99 | proc suffixAsInt*(s, prefix: cstring): int = parseInt(suffix(s, prefix))
100 |
101 | #proc ceil(f: float): int {.importc: "Math.ceil", nodecl.}
102 |
103 | proc realtimeInput*(val: cstring; action: EventHandler): VNode =
104 | var timer: Timeout
105 | proc onkeyup(ev: Event; n: VNode) =
106 | proc wrapper() = keyeventBody()
107 |
108 | if timer != nil: clearTimeout(timer)
109 | timer = setTimeout(wrapper, 400)
110 | result = tree(VNodeKind.input, [(cstring"type", cstring"text")])
111 | result.value = val
112 | result.addEventListener(EventKind.onkeyup, onkeyup)
113 |
114 | proc enterInput*(id, val: cstring; action: EventHandler): VNode =
115 | proc onkeyup(ev: Event; n: VNode) =
116 | if ev.keyCode == 13: keyeventBody()
117 |
118 | result = tree(VNodeKind.input, [(cstring"type", cstring"text")])
119 | result.id = id
120 | result.value = val
121 | result.addEventListener(EventKind.onkeyup, onkeyup)
122 |
123 | proc setOnEnter*(n: VNode; action: EventHandler) =
124 | proc onkeyup(ev: Event; n: VNode) =
125 | if ev.keyCode == 13: keyeventBody()
126 | n.addEventListener(EventKind.onkeyup, onkeyup)
127 |
128 | proc setOnscroll*(action: proc(min, max: VKey; diff: int)) =
129 | var oldY = window.pageYOffset
130 |
131 | proc wrapper(ev: Event) =
132 | let dir = window.pageYOffset - oldY
133 | if dir == 0: return
134 |
135 | var a = VKey high(int)
136 | var b = VKey 0
137 | var h, count: int
138 | document.visibleKeys(a, b, h, count)
139 | let avgh = h / count
140 | let diff = toInt(dir.float / avgh)
141 | if diff != 0:
142 | oldY = window.pageYOffset
143 | action(a, b, diff)
144 | redraw()
145 |
146 | document.addEventListener("scroll", wrapper)
147 |
148 | when false:
149 | var plugins {.exportc.}: seq[(string, proc())] = @[]
150 |
151 | proc onInput(val: cstring) =
152 | kout val
153 | if val == "dyn":
154 | kout(plugins.len)
155 | if plugins.len > 0:
156 | plugins[0][1]()
157 |
158 | var
159 | images: seq[cstring] = @[cstring"a", "b", "c", "d"]
160 |
161 | proc carousel*(): VNode =
162 | var currentIndex = 0
163 |
164 | proc next(ev: Event; n: VNode) =
165 | currentIndex = (currentIndex + 1) mod images.len
166 |
167 | proc prev(ev: Event; n: VNode) =
168 | currentIndex = (currentIndex - 1) mod images.len
169 |
170 | result = buildHtml(tdiv):
171 | text images[currentIndex]
172 | button(onclick = next):
173 | text "Next"
174 | button(onclick = prev):
175 | text "Previous"
176 |
177 | #proc targetElem*(e: Event): Element = cast[Element](e.target)
178 |
179 | #proc getElementsByClassName*(cls: cstring): seq[Element] {.importc:
180 | # "document.getElementsByClassName", nodecl.}
181 | #proc textContent(e: Node): cstring {.
182 | # importcpp: "#.textContent", nodecl.}
183 |
184 | proc isElementInViewport(el: Node; h: var int): bool =
185 | let rect = el.getBoundingClientRect()
186 | h = rect.bottom - rect.top
187 | result = rect.top >= 0 and rect.left >= 0 and
188 | rect.bottom <= clientHeight() and
189 | rect.right <= clientWidth()
190 |
191 | proc visibleKeys(e: Node; a, b: var VKey; h, count: var int) =
192 | # we only care about nodes that have a key:
193 | var hh = 0
194 | # do not recurse if there is a 'key' field already:
195 | if e.key >= 0:
196 | if isElementInViewport(e, hh):
197 | inc count
198 | inc h, hh
199 | a = min(a, e.key)
200 | b = max(b, e.key)
201 | else:
202 | for i in 0..
2 |
3 |
4 |
5 |
6 | Refs
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/experiments/refs/demo.nim:
--------------------------------------------------------------------------------
1 | include karax/prelude
2 | import sugar, sequtils
3 |
4 | type
5 | CustomRef = ref object of VComponent
6 |
7 | method onAttach(r: CustomRef) =
8 | kout(cstring"custom ref attached")
9 |
10 | method onDetach(r: CustomRef) =
11 | kout(cstring"custom ref detached")
12 |
13 | var
14 | modelData = @[5, 2, 4]
15 |
16 | refA = VComponent()
17 | refB = VComponent()
18 | refC = CustomRef()
19 | refSeq = newSeq[VComponent]()
20 |
21 | proc onClick(ev: Event, n: VNode) =
22 | modelData.add(0)
23 |
24 | proc showRefs() =
25 | kout(refA)
26 | kout(refB)
27 | kout(refC)
28 | for i in 0 ..< refSeq.len:
29 | kout(refSeq[i].vnode)
30 | #kout(refSeq.map(nref => nref.vnode))
31 |
32 | proc secureRefSlot(i: int): VComponent =
33 | while refSeq.len <= i:
34 | refSeq.add(VComponent())
35 | result = refSeq[i]
36 |
37 | proc view(): VNode =
38 | result = buildHtml():
39 | tdiv:
40 | button(onclick=onClick):
41 | text "click me"
42 | # storing refs to single elements is straightforward now
43 | if modelData.len mod 2 == 0:
44 | tdiv(nref=refA):
45 | text "A"
46 | tdiv(nref=refB):
47 | text "B"
48 | tdiv(nref=refC):
49 | text "C"
50 | # It's a bit more tricky when containers are involved:
51 | for i, x in modelData.pairs:
52 | tdiv(nref=secureRefSlot(i)):
53 | text "List item: " & $x
54 |
55 | proc renderer(): VNode =
56 | view()
57 |
58 | setRenderer(renderer, "ROOT", showRefs)
59 |
--------------------------------------------------------------------------------
/experiments/refs/demo.nims:
--------------------------------------------------------------------------------
1 | --path: "../../src"
2 |
--------------------------------------------------------------------------------
/experiments/scrollapp.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Todo app
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/experiments/scrollapp.nim:
--------------------------------------------------------------------------------
1 |
2 | import vdom, karax, karaxdsl, jdict, jstrutils, compact
3 |
4 | type
5 | Filter = enum
6 | all, active, completed
7 |
8 | var
9 | entries: seq[(cstring, bool)]
10 | visibleA = 0
11 | visibleB = 10
12 | selectedEntry = -1
13 | filter: Filter
14 |
15 | proc onTodoEnter(ev: Event; n: VNode) =
16 | entries.add((n.value, false))
17 | n.value = ""
18 |
19 | proc removeHandler(ev: Event; n: VNode) =
20 | entries[n.key] = (cstring(nil), false)
21 |
22 | proc editHandler(ev: Event; n: VNode) =
23 | selectedEntry = n.key
24 |
25 | proc focusLost(ev: Event; n: VNode) = selectedEntry = -1
26 |
27 | proc editEntry(ev: Event; n: VNode) =
28 | entries[n.key][0] = n.value
29 | selectedEntry = -1
30 |
31 | proc toggleEntry(ev: Event; n: VNode) =
32 | let id = n.key
33 | entries[id][1] = not entries[id][1]
34 |
35 | proc onAllDone(ev: Event; n: VNode) =
36 | entries = @[]
37 | selectedEntry = -1
38 |
39 | proc clearCompleted(ev: Event, n: VNode) =
40 | for i in 0.. 0:
123 | # scroll downwards:
124 | visibleA = max(a-overshoot2, 0)
125 | visibleB = min(b+diff+overshoot1, entries.len-1)
126 | elif diff < 0:
127 | # scroll upwards:
128 | visibleA = max(a+diff-overshoot1, 0)
129 | visibleB = min(b+overshoot2, entries.len-1)
130 | )
131 |
132 | setOnHashChange(proc(hash: cstring) =
133 | if hash == cstring"#/": filter = all
134 | elif hash == cstring"#/completed": filter = completed
135 | elif hash == cstring"#/active": filter = active
136 | )
137 | setRenderer createDom
138 |
139 | proc onload(session: cstring) {.exportc.} =
140 | for i in 0..visibleB:
141 | entries.add((cstring"Entry " & &i, false))
142 | init()
143 |
--------------------------------------------------------------------------------
/experiments/scrollapp_simple.nim:
--------------------------------------------------------------------------------
1 | ## Example that shows how to accomplish an "infinitely scrolling" app.
2 |
3 | include karax / prelude
4 | import karax / [kdom, vstyles]
5 |
6 | var entries: seq[cstring] = @[]
7 | for i in 1..500:
8 | entries.add(cstring("Entry ") & &i)
9 |
10 | proc scrollEvent(ev: Event; n: VNode) =
11 | let d = n.dom
12 | if d != nil and inViewport(d.lastChild):
13 | # "load" more data:
14 | for i in 1..50:
15 | entries.add(cstring("Loaded Entry ") & &i)
16 |
17 | proc createDom(): VNode =
18 | result = buildHtml():
19 | tdiv(onscroll=scrollEvent, style=style(
20 | (StyleAttr.height, cstring"400px"), (StyleAttr.overflow, cstring"scroll"))):
21 | for x in entries:
22 | tdiv:
23 | text x
24 |
25 | setRenderer createDom
26 |
--------------------------------------------------------------------------------
/experiments/style.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | .todomvc-wrapper {
8 | visibility: visible !important;
9 | }
10 |
11 | button {
12 | margin: 0;
13 | padding: 0;
14 | border: 0;
15 | background: none;
16 | font-size: 100%;
17 | vertical-align: baseline;
18 | font-family: inherit;
19 | font-weight: inherit;
20 | color: inherit;
21 | -webkit-appearance: none;
22 | appearance: none;
23 | -webkit-font-smoothing: antialiased;
24 | -moz-font-smoothing: antialiased;
25 | font-smoothing: antialiased;
26 | }
27 |
28 | body {
29 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
30 | line-height: 1.4em;
31 | background: #f5f5f5;
32 | color: #4d4d4d;
33 | min-width: 230px;
34 | max-width: 550px;
35 | margin: 0 auto;
36 | -webkit-font-smoothing: antialiased;
37 | -moz-font-smoothing: antialiased;
38 | font-smoothing: antialiased;
39 | font-weight: 300;
40 | }
41 |
42 | button,
43 | input[type="checkbox"] {
44 | outline: none;
45 | }
46 |
47 | .hidden {
48 | display: none;
49 | }
50 |
51 | .todoapp {
52 | background: #fff;
53 | margin: 130px 0 40px 0;
54 | position: relative;
55 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
56 | 0 25px 50px 0 rgba(0, 0, 0, 0.1);
57 | }
58 |
59 | .todoapp input::-webkit-input-placeholder {
60 | font-style: italic;
61 | font-weight: 300;
62 | color: #e6e6e6;
63 | }
64 |
65 | .todoapp input::-moz-placeholder {
66 | font-style: italic;
67 | font-weight: 300;
68 | color: #e6e6e6;
69 | }
70 |
71 | .todoapp input::input-placeholder {
72 | font-style: italic;
73 | font-weight: 300;
74 | color: #e6e6e6;
75 | }
76 |
77 | .todoapp h1 {
78 | position: absolute;
79 | top: -155px;
80 | width: 100%;
81 | font-size: 100px;
82 | font-weight: 100;
83 | text-align: center;
84 | color: rgba(175, 47, 47, 0.15);
85 | -webkit-text-rendering: optimizeLegibility;
86 | -moz-text-rendering: optimizeLegibility;
87 | text-rendering: optimizeLegibility;
88 | }
89 |
90 | .new-todo,
91 | .edit {
92 | position: relative;
93 | margin: 0;
94 | width: 100%;
95 | font-size: 24px;
96 | font-family: inherit;
97 | font-weight: inherit;
98 | line-height: 1.4em;
99 | border: 0;
100 | outline: none;
101 | color: inherit;
102 | padding: 6px;
103 | border: 1px solid #999;
104 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
105 | box-sizing: border-box;
106 | -webkit-font-smoothing: antialiased;
107 | -moz-font-smoothing: antialiased;
108 | font-smoothing: antialiased;
109 | }
110 |
111 | .new-todo {
112 | padding: 16px 16px 16px 60px;
113 | border: none;
114 | background: rgba(0, 0, 0, 0.003);
115 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
116 | }
117 |
118 | .main {
119 | position: relative;
120 | z-index: 2;
121 | border-top: 1px solid #e6e6e6;
122 | }
123 |
124 | label[for='toggle-all'] {
125 | display: none;
126 | }
127 |
128 | .toggle-all {
129 | position: absolute;
130 | top: -55px;
131 | left: -12px;
132 | width: 60px;
133 | height: 34px;
134 | text-align: center;
135 | border: none; /* Mobile Safari */
136 | }
137 |
138 | .toggle-all:before {
139 | content: '>';
140 | font-size: 22px;
141 | color: #e6e6e6;
142 | padding: 10px 27px 10px 27px;
143 | }
144 |
145 | .toggle-all:checked:before {
146 | color: #737373;
147 | }
148 |
149 | .todo-list {
150 | margin: 0;
151 | padding: 0;
152 | list-style: none;
153 | }
154 |
155 | .todo-list li {
156 | position: relative;
157 | font-size: 24px;
158 | border-bottom: 1px solid #ededed;
159 | }
160 |
161 | .todo-list li:last-child {
162 | border-bottom: none;
163 | }
164 |
165 | .todo-list li.editing {
166 | border-bottom: none;
167 | padding: 0;
168 | }
169 |
170 | .todo-list li.editing .edit {
171 | display: block;
172 | width: 506px;
173 | padding: 13px 17px 12px 17px;
174 | margin: 0 0 0 43px;
175 | }
176 |
177 | .todo-list li.editing .view {
178 | display: none;
179 | }
180 |
181 | .todo-list li .toggle {
182 | text-align: center;
183 | width: 40px;
184 | /* auto, since non-WebKit browsers doesn't support input styling */
185 | height: auto;
186 | position: absolute;
187 | top: 0;
188 | bottom: 0;
189 | margin: auto 0;
190 | border: none; /* Mobile Safari */
191 | -webkit-appearance: none;
192 | appearance: none;
193 | }
194 |
195 | .todo-list li .toggle:after {
196 | content: url('data:image/svg+xml,');
197 | }
198 |
199 | .todo-list li .toggle:checked:after {
200 | content: url('data:image/svg+xml,');
201 | }
202 |
203 | .todo-list li label {
204 | white-space: pre-line;
205 | word-break: break-all;
206 | padding: 15px 60px 15px 15px;
207 | margin-left: 45px;
208 | display: block;
209 | line-height: 1.2;
210 | transition: color 0.4s;
211 | }
212 |
213 | .todo-list li.completed label {
214 | color: #d9d9d9;
215 | text-decoration: line-through;
216 | }
217 |
218 | .todo-list li .destroy {
219 | display: none;
220 | position: absolute;
221 | top: 0;
222 | right: 10px;
223 | bottom: 0;
224 | width: 40px;
225 | height: 40px;
226 | margin: auto 0;
227 | font-size: 30px;
228 | color: #cc9a9a;
229 | margin-bottom: 11px;
230 | transition: color 0.2s ease-out;
231 | }
232 |
233 | .todo-list li .destroy:hover {
234 | color: #af5b5e;
235 | }
236 |
237 | .todo-list li .destroy:after {
238 | content: 'x';
239 | }
240 |
241 | .todo-list li:hover .destroy {
242 | display: block;
243 | }
244 |
245 | .todo-list li.editing:last-child {
246 | margin-bottom: -1px;
247 | }
248 |
249 | .footer {
250 | color: #777;
251 | padding: 10px 15px;
252 | height: 20px;
253 | text-align: center;
254 | border-top: 1px solid #e6e6e6;
255 | }
256 |
257 | .footer:before {
258 | content: '';
259 | position: absolute;
260 | right: 0;
261 | bottom: 0;
262 | left: 0;
263 | height: 50px;
264 | overflow: hidden;
265 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
266 | 0 8px 0 -3px #f6f6f6,
267 | 0 9px 1px -3px rgba(0, 0, 0, 0.2),
268 | 0 16px 0 -6px #f6f6f6,
269 | 0 17px 2px -6px rgba(0, 0, 0, 0.2);
270 | }
271 |
272 | .todo-count {
273 | float: left;
274 | text-align: left;
275 | }
276 |
277 | .todo-count strong {
278 | font-weight: 300;
279 | }
280 |
281 | .filters {
282 | margin: 0;
283 | padding: 0;
284 | list-style: none;
285 | position: absolute;
286 | right: 0;
287 | left: 0;
288 | }
289 |
290 | .filters li {
291 | display: inline;
292 | }
293 |
294 | .filters li a {
295 | color: inherit;
296 | margin: 3px;
297 | padding: 3px 7px;
298 | text-decoration: none;
299 | border: 1px solid transparent;
300 | border-radius: 3px;
301 | }
302 |
303 | .filters li a.selected,
304 | .filters li a:hover {
305 | border-color: rgba(175, 47, 47, 0.1);
306 | }
307 |
308 | .filters li a.selected {
309 | border-color: rgba(175, 47, 47, 0.2);
310 | }
311 |
312 | .clear-completed,
313 | html .clear-completed:active {
314 | float: right;
315 | position: relative;
316 | line-height: 20px;
317 | text-decoration: none;
318 | cursor: pointer;
319 | position: relative;
320 | }
321 |
322 | .clear-completed:hover {
323 | text-decoration: underline;
324 | }
325 |
326 | .info {
327 | margin: 65px auto 0;
328 | color: #bfbfbf;
329 | /* font-size: 10px; */
330 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
331 | text-align: center;
332 | }
333 |
334 | .info p {
335 | line-height: 1;
336 | }
337 |
338 | .info a {
339 | color: inherit;
340 | text-decoration: none;
341 | font-weight: 400;
342 | }
343 |
344 | .info a:hover {
345 | text-decoration: underline;
346 | }
347 |
348 | /*
349 | Hack to remove background from Mobile Safari.
350 | Can't use it globally since it destroys checkboxes in Firefox
351 | */
352 | @media screen and (-webkit-min-device-pixel-ratio:0) {
353 | .toggle-all,
354 | .todo-list li .toggle {
355 | background: none;
356 | }
357 |
358 | .todo-list li .toggle {
359 | height: 40px;
360 | }
361 |
362 | .toggle-all {
363 | -webkit-transform: rotate(90deg);
364 | transform: rotate(90deg);
365 | -webkit-appearance: none;
366 | appearance: none;
367 | }
368 | }
369 |
370 | @media (max-width: 430px) {
371 | .footer {
372 | height: 50px;
373 | }
374 |
375 | .filters {
376 | bottom: 10px;
377 | }
378 | }
--------------------------------------------------------------------------------
/experiments/trello/trello.nim:
--------------------------------------------------------------------------------
1 |
2 | import knete, widgets
3 | import karax / jstrutils
4 | import std / dom
5 |
6 | type
7 | Attachable* = ref object of RootObj ## an element that is attachable to
8 | ## DOM elements.
9 | attachedTo*: seq[Element]
10 |
11 | TaskId = int
12 | Task = ref object
13 | id: TaskId
14 | name, desc: kstring
15 |
16 | ColumnId = distinct int
17 | Column = ref object of Attachable
18 | id: ColumnId
19 | header: kstring
20 | tasks: seq[Task]
21 |
22 | Board = ref object
23 | columns: seq[Column]
24 |
25 | var b = Board(columns: @[Column(id: ColumnId 0, header: "To Do"),
26 | Column(id: ColumnId 1, header: "Doing"),
27 | Column(id: ColumnId 2, header: "Done")])
28 |
29 | proc removeTask(t: TaskId) =
30 | for c in b.columns:
31 | for i in 0 ..< c.tasks.len:
32 | if c.tasks[i].id == t:
33 | delete(c.tasks, i)
34 | break
35 |
36 | proc moveTask(t: TaskId; dest: Column) =
37 | for c in b.columns:
38 | for i in 0 ..< c.tasks.len:
39 | let task = c.tasks[i]
40 | if task.id == t:
41 | delete(c.tasks, i)
42 | dest.tasks.add task
43 | return
44 |
45 | var nextTaskId = 0 ## negative because they are not yet backed
46 | ## up by the database
47 |
48 | proc createTask(c: Column; name, desc: kstring): Task =
49 | dec nextTaskId
50 | result = Task(id: TaskId nextTaskId, name: name, desc: desc)
51 | c.tasks.add result
52 |
53 | # --------------------- UI -----------------------------------------------
54 |
55 | proc renderTask(t: Task): Element =
56 | proc dodelete =
57 | removeTask t.id
58 | delete result
59 |
60 | proc dragstart(ev: Event) =
61 | ev.prepareDragData("taskid", ev.target.id)
62 |
63 | result = buildHtml():
64 | tdiv(draggable="true", ondragstart=dragstart, id = &t.id):
65 | bindField t.name
66 | br()
67 | bindField t.desc
68 | button(onclick = dodelete):
69 | text cross
70 |
71 | type
72 | NewTaskPanel = object
73 | c: Column
74 | e: Element
75 |
76 | proc open(p: var NewTaskPanel; c: Column) =
77 | p.e.style.display = "block"
78 | p.c = c
79 |
80 | proc newTaskDialog(): NewTaskPanel =
81 | var nameInp = buildHtml():
82 | input(setFocus = true)
83 | var descInp = buildHtml():
84 | input()
85 |
86 | proc close =
87 | result.e.style.display = "none"
88 |
89 | proc submit =
90 | let t = createTask(result.c, nameInp.value, descInp.value)
91 | close()
92 | result.c.attachedTo[0].add renderTask(t)
93 |
94 | result.e = buildHtml():
95 | tdiv(style={display: "none", position: "fixed",
96 | left: "0", top: "0", width: "100%", height: "100%",
97 | overflow: "auto",
98 | backgroundColor: "rgb(0,0,0)",
99 | backgroundColor: "rgba(0,0,0,0.4)", zIndex: "1"}):
100 | tdiv(style={backgroundColor: "#fefefe",
101 | margin: "15% auto", padding: "20px", border: "1px solid #888",
102 | width: "80%"}):
103 | span(onclick = close, style={color: "#aaa",
104 | cssFloat: "right",
105 | fontSize: "28px",
106 | fontWeight: "bold"}):
107 | text cross
108 | p:
109 | text "Task name"
110 | nameInp
111 | p:
112 | text "Task description"
113 | descInp
114 | span(onclick = submit, style={color: "#0f0",
115 | cssFloat: "left",
116 | fontWeight: "bold"}):
117 | text "Submit"
118 |
119 | var dialog = newTaskDialog()
120 |
121 | proc renderColumn(c: Column): Element =
122 | proc doadd(c: Column): proc () =
123 | result = proc() = dialog.open(c)
124 |
125 | proc allowDrop(ev: Event) = ev.preventDefault()
126 | proc drop(ev: Event) =
127 | ev.preventDefault()
128 | let data = ev.recvDragData("taskid")
129 | moveTask(parseInt data, c)
130 | Element(ev.target).up("mycolumn").add(getElementById(data))
131 |
132 | result = buildHtml():
133 | tdiv(class = "mycolumn", style = {cssFloat: "left", width: "20%"},
134 | ondrop = drop, ondragover = allowDrop):
135 | tdiv(class = "myheader"):
136 | bindField c.header
137 | span(onclick = doadd(c)):
138 | text plus
139 | for t in c.tasks:
140 | renderTask(t)
141 | c.attachedTo.add result
142 |
143 | proc renderBoard(b: Board): Element =
144 | result = buildHtml(tdiv):
145 | dialog.e
146 | for c in b.columns:
147 | renderColumn(c)
148 |
149 | proc main(hashPart: kstring): Element =
150 | result = renderBoard(b)
151 |
152 | setInitializer main
153 |
--------------------------------------------------------------------------------
/experiments/trello/widgets.nim:
--------------------------------------------------------------------------------
1 | ## Common widget implemenations
2 |
3 | import knete
4 | import std / dom
5 |
6 | const
7 | cross* = kstring"x"
8 | plus* = kstring"+"
9 |
10 | const
11 | LEFT* = 37
12 | UP* = 38
13 | RIGHT* = 39
14 | DOWN* = 40
15 | TAB* = 9
16 | ESC* = 27
17 | ENTER* = 13
18 |
19 | proc editable*(x: kstring; onchanged: proc (value: kstring);
20 | isEdited = false): Element =
21 | proc onenter(ev: Event) = Element(ev.target).blur()
22 | proc submit(ev: Event) =
23 | onchanged(ev.target.value)
24 | # close the loop: This is a common pattern in Knete.
25 | replace(result, editable(ev.target.value, onchanged, false))
26 |
27 | proc makeEditable() =
28 | # close the loop: This is a common pattern in Knete.
29 | replace(result, editable(x, onchanged, true))
30 |
31 | result = buildHtml():
32 | if isEdited:
33 | input(class = "edit",
34 | onblur = submit,
35 | onkeyupenter = onenter,
36 | value = x,
37 | setFocus = true)
38 | else:
39 | span(onclick = makeEditable):
40 | text x
41 |
42 | template bindField*(field): Element =
43 | editable field, proc (value: kstring) =
44 | field = value
45 |
--------------------------------------------------------------------------------
/guide/Installation.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | First, make sure that you have Nim installed.
4 | Then, run `nimble install karax` to install Karax.
5 |
6 | Afterwards, run `karun` to make sure that you also have Karax's build tool installed.
7 | Karax uses Nim's JS backend to compile, so most modern browsers should be supported.
8 | Ready to learn more? Head over to the Introduction.
9 |
--------------------------------------------------------------------------------
/guide/Introduction.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | ## What is Karax?
4 | Karax is a framework for developing SPAs (single page applications) using Nim.
5 | It is designed to be easy to use and fast, by using a virtual DOM model similar to React.
6 |
7 | ## Getting Started
8 | > We assume that the reader has knowledge of basic HTML, CSS, and Nim.
9 | > Knowledge of Javascript (specifically events) is reccomended, but not required.
10 |
11 | We'll be using `karun` for most of this guide, although it is possible to compile Karax applications using `nim js`
12 |
13 | Here's a simple example:
14 | ```nim
15 | include karax/prelude # imports many of the basic Karax modules
16 |
17 | proc createDom(): VNode = # define a function to return our HTML nodes
18 | buildHtml(p): # create a paragraph element
19 | text "Welcome to Karax!" # set the text inside of the paragraph element
20 |
21 | setRenderer createDom # tell Karax to use function to render
22 | ```
23 |
24 | Save this file as `ìndex.nim`. Then, run
25 | ```
26 | karun -r index.nim
27 | ```
28 | This should compile the file using the `js` backend from Nim and open the file in your default browser!
29 | Note that you can also pass in the `-w` flag to make it so that whenever you save the `ìndex.nim` file, it will automatically rebuild and refresh the page.
30 |
31 | The syntax here shouldn't be too confusing.
32 | Karax comes with a built in DSL (domain specific language) to aid in generating HTML nodes.
33 |
34 | If you want to bind to an HTML attribute, you can do the following:
35 | ```nim
36 | include karax/prelude
37 |
38 | proc createDom(): VNode =
39 | buildHtml(p(title = "Here is some help text!")):
40 | text "Hover over me to see the help text!"
41 |
42 | setRenderer createDom
43 | ```
44 | Pretty simple, right? You can specify any HTML attribute that you want here.
45 |
46 | ### Conditionals and Loops
47 | It's simple to toggle whether an element exists as well.
48 |
49 | ```nim
50 | include karax/prelude
51 | var show = true
52 | proc createDom(): VNode =
53 | buildHtml(tdiv):
54 | if show:
55 | p:
56 | text "Now you see me!"
57 | else:
58 | p:
59 | text "Now you dont!"
60 |
61 | setRenderer createDom
62 | ```
63 | Go ahead and try running this. Change `show` to `false`, and you should see the text change as well!
64 | Standard Nim if statements work just fine and are handled by the DSL.
65 |
66 | There is one thing that is unfamiliar here though: What is the `tdiv` tag?
67 | Since Nim has `div` as a reserved keyword, Karax defines tdiv to refer to the `div` element.
68 | You can see a list of all of the tags along with their mapping at the top of this file: [vdom.nim](https://github.com/karaxnim/karax/blob/master/karax/vdom.nim)
69 |
70 | How do we display a list of elements in Karax? As you might expect, using a Nim for loop.
71 |
72 | ```nim
73 | include karax/prelude
74 | var list = @[kstring"Apples", "Oranges", "Bananas"]
75 | proc createDom(): VNode =
76 | buildHtml(tdiv):
77 | for fruit in list:
78 | p:
79 | text fruit
80 | text " is a fruit"
81 |
82 | setRenderer createDom
83 | ```
84 | It just works! Note that we used multiple text functions to add more text together.
85 | It is also possible to do this using string concatenation or interpolation, but this is simpler.
86 |
87 | #### `kstring` vs `string` vs `cstring`
88 | As you may have noticed above, we made `list` into a seq[kstring] rather than a seq[string].
89 | If you've ever worked with Nim's `js` backend, you would know that Javascript strings are different from Nim strings.
90 | Nim uses the `cstring` type to denote a "compatible string".
91 | In our case, this corresponds to the native Javascript string type.
92 | In fact, if you try using Nim's string type on the Javascript backend, you'll get something like:
93 | ```
94 | [45, 67, 85, 34, ...]
95 | ```
96 | This is how Nim strings are handle internally - a sequence of numbers.
97 | We use `cstring` to avoid taking a performance penalty when working with strings, as the native string type is faster than a list of numbers.
98 |
99 | What is `kstring` then? `kstring` corresponds to a `cstring` when compiled using the `js` backend, and a `string` otherwise.
100 | This makes it much easier to write code that can be used on multiple platforms.
101 |
102 | ### Handling User Input
103 | Karax allows you to simply utilize existing DOM events to handle user input.
104 |
105 | ```nim
106 | include karax/prelude
107 | import algorithm
108 | var message = "Karax is fun!"
109 | proc createDom(): VNode =
110 | buildHtml(tdiv):
111 | p:
112 | text message
113 | button:
114 | text "Click me to reverse!"
115 | proc onclick =
116 | message.reverse()
117 |
118 | setRenderer createDom
119 | ```
120 | Clicking that button causes the onclick event to fire, which reverses our string.
121 | Note that we treat `message` as a string - that way we can reverse the string using Nim's `algorithm` module.
122 |
123 | Karax can work with text inputs as well!
124 |
125 | ```nim
126 | include karax/prelude
127 | var message = kstring"Karax is fun!"
128 | proc createDom(): VNode =
129 | buildHtml(tdiv):
130 | p:
131 | text message
132 | input(value = message):
133 | proc oninput(e: Event, n: VNode) =
134 | message = n.value
135 |
136 | setRenderer createDom
137 | ```
138 | No manual DOM manipulation required!
139 | Just change the variable and everything magically updates.
140 |
141 | You may notice that this time our `proc` has two arguments.
142 | You can see the Karax documentation for information on VNode, but the `Event` type is from the `dom` module in the stdlib.
143 | It is defined similarly to a DOM event in Javascript.
144 |
145 | ### Composing and Reusability
146 | Unlike other web frameworks, Karax doesn't have explicit components.
147 | Instead, it gives you the freedom to organize your code how you want.
148 |
149 | So, to mimic what components do we can just use functions.
150 |
151 | ```nim
152 | include karax/prelude
153 | type Task = ref object
154 | id: int
155 | text: kstring
156 | var tasks = @[
157 | Task(id: 0, text: "Buy milk"),
158 | Task(id: 1, text: "Clean table"),
159 | Task(id: 2, text: "Call mom")
160 | ]
161 | proc render(t: Task): VNode =
162 | buildHtml(li):
163 | text t.text
164 |
165 | proc createDom(): VNode =
166 | buildHtml(tdiv):
167 | ol:
168 | for task in tasks:
169 | task.render()
170 |
171 | setRenderer createDom
172 | ```
173 | With this, we can easily divide up parts of a more complex application.
174 | Function arguments can take the place of "props", and we're not constrained by "components" in any way.
175 |
176 | We've briefly covered many of the features that Karax offers.
177 | Interested in more? Continue reading for more advanced topics!
178 |
--------------------------------------------------------------------------------
/karax.nimble:
--------------------------------------------------------------------------------
1 | # Package
2 |
3 | version = "1.5.0"
4 | author = "Andreas Rumpf"
5 | description = "Karax is a framework for developing single page applications in Nim."
6 | license = "MIT"
7 |
8 | # Dependencies
9 |
10 | requires "nim >= 0.18.0"
11 | requires "ws"
12 | requires "dotenv >= 2.0.0"
13 | skipDirs = @["examples", "experiments", "tests"]
14 |
15 | bin = @["karax/tools/karun"]
16 | installExt = @["nim"]
17 |
--------------------------------------------------------------------------------
/karax/autocomplete.nim:
--------------------------------------------------------------------------------
1 |
2 | import karax, karaxdsl, vdom, kdom, jstrutils
3 |
4 | type
5 | AutocompleteState* = ref object
6 | choices, candidates: seq[cstring]
7 | selected, maxMatches: int
8 | showCandidates, controlPressed: bool
9 |
10 | proc newAutocomplete*(choices: seq[cstring]; maxMatches = 5): AutocompleteState =
11 | ## Creates a new state for the autocomplete widget. ``maxMatches`` is the maximum
12 | ## number of elements to show.
13 | AutocompleteState(choices: choices, candidates: @[],
14 | selected: -1, maxMatches: maxMatches, showCandidates: false,
15 | controlPressed: false)
16 |
17 | proc autocomplete*(s: AutocompleteState; onselection: proc(s: cstring)): VNode =
18 | var inp: VNode
19 |
20 | proc commit(ev: Event) =
21 | s.showCandidates = false
22 | onselection(inp.dom.value)
23 | when false:
24 | if inp.dom != nil:
25 | echo "setting to A ", inp.dom.value.isNil
26 | result.text = inp.dom.value
27 | else:
28 | echo "setting to B ", inp.text.isNil
29 | result.text = inp.text
30 | for e in result.events:
31 | if e[0] == EventKind.onchange:
32 | e[1](ev, result)
33 |
34 | proc onkeyuplater(ev: Event; n: VNode) =
35 | if not s.controlPressed:
36 | let v = n.value
37 | s.candidates.setLen 0
38 | for c in s.choices:
39 | if v.len == 0 or c.containsIgnoreCase(v): s.candidates.add(c)
40 |
41 | proc onkeydown(ev: Event; n: VNode) =
42 | const
43 | LEFT = 37
44 | UP = 38
45 | RIGHT = 39
46 | DOWN = 40
47 | TAB = 9
48 | ESC = 27
49 | ENTER = 13
50 | # UP: Move focus to the previous item. If on first item, move focus to the input.
51 | # If on the input, move focus to last item.
52 | # DOWN: Move focus to the next item. If on last item, move focus to the input.
53 | # If on the input, move focus to the first item.
54 | # ESCAPE: Close the menu.
55 | # ENTER: Select the currently focused item and close the menu.
56 | # TAB: Select the currently focused item, close the menu, and
57 | # move focus to the next focusable element
58 | s.controlPressed = false
59 | case ev.keyCode
60 | of UP:
61 | s.controlPressed = true
62 | s.showCandidates = true
63 | if s.selected > 0:
64 | dec s.selected
65 | n.setInputText s.candidates[s.selected]
66 | of DOWN:
67 | s.controlPressed = true
68 | s.showCandidates = true
69 | if s.selected < s.candidates.len - 1:
70 | inc s.selected
71 | n.setInputText s.candidates[s.selected]
72 | of ESC:
73 | s.showCandidates = false
74 | s.controlPressed = true
75 | of ENTER:
76 | s.controlPressed = true
77 | # inp.setInputText s.choices[i]
78 | commit(ev)
79 | else:
80 | discard
81 |
82 | proc window(s: AutocompleteState): (int, int) =
83 | var first, last: int
84 | if s.selected >= 0:
85 | first = s.selected - (s.maxMatches div 2)
86 | last = s.selected + (s.maxMatches div 2)
87 | if first < 0: first = 0
88 | # too few because we had to trim first?
89 | if (last - first + 1) < s.maxMatches: last = first + s.maxMatches - 1
90 | else:
91 | first = 0
92 | last = s.maxMatches - 1
93 | if last > high(s.candidates): last = high(s.candidates)
94 | # still too few because we're at the end?
95 | if (last - first + 1) < s.maxMatches:
96 | first = last - s.maxMatches + 1
97 | if first < 0: first = 0
98 |
99 | result = (first, last)
100 |
101 | inp = buildHtml:
102 | input(onkeyuplater = onkeyuplater,
103 | onkeydown = onkeydown,
104 | # onblur = proc (ev: Event; n: VNode) = commit(ev),
105 | onfocus = proc (ev: Event; n: VNode) =
106 | onkeyuplater(ev, n)
107 | s.showCandidates = true)
108 |
109 | proc select(i: int): proc(ev: Event; n: VNode) =
110 | result = proc(ev: Event; n: VNode) =
111 | s.selected = i
112 | s.showCandidates = false
113 | inp.setInputText s.choices[i]
114 | commit(ev)
115 |
116 | result = buildHtml(table):
117 | tr:
118 | td:
119 | inp
120 | if s.showCandidates:
121 | let (first, last) = window(s)
122 | for i in first..last:
123 | tr:
124 | td(onclick = select(i), class = cstring"button " &
125 | (if i == s.selected: cstring"is-primary" else: "")):
126 | text s.candidates[i]
127 |
128 | when isMainModule:
129 | const suggestions = @[cstring"ActionScript",
130 | "AppleScript",
131 | "Asp",
132 | "BASIC",
133 | "C",
134 | "C++",
135 | "Clojure",
136 | "COBOL",
137 | "Erlang",
138 | "Fortran",
139 | "Groovy",
140 | "Haskell",
141 | "Java",
142 | "JavaScript",
143 | "Lisp",
144 | "Nim",
145 | "Perl",
146 | "PHP",
147 | "Python",
148 | "Ruby",
149 | "Scala",
150 | "Scheme"]
151 |
152 | var s = newAutocomplete(suggestions)
153 |
154 | proc main(): VNode =
155 | result = buildHtml(tdiv):
156 | autocomplete(s, proc (s: cstring) = echo "now ", s)#:
157 | # proc onchange(ev: Event; n: VNode) =
158 | # echo "now selected ", n.kind
159 |
160 | setRenderer(main)
161 |
--------------------------------------------------------------------------------
/karax/compact.nim:
--------------------------------------------------------------------------------
1 | ## Components in Karax are built by the ``.component`` macro annotation.
2 |
3 | when defined(js):
4 | import jdict, kdom
5 |
6 | import macros, vdom, tables, strutils, kbase
7 |
8 | when defined(js):
9 | var
10 | vcomponents* = newJDict[cstring, proc(args: seq[VNode]): VNode]()
11 | else:
12 | var
13 | vcomponents* = newTable[kstring, proc(args: seq[VNode]): VNode]()
14 |
15 | type
16 | ComponentKind* {.pure.} = enum
17 | None,
18 | Tag,
19 | VNode
20 |
21 | var
22 | allcomponents {.compileTime.} = initTable[string, ComponentKind]()
23 |
24 | proc isComponent*(x: string): ComponentKind {.compileTime.} =
25 | allcomponents.getOrDefault(x)
26 |
27 | proc addTags() {.compileTime.} =
28 | let x = (bindSym"VNodeKind").getTypeImpl
29 | expectKind(x, nnkEnumTy)
30 | for i in ord(VNodeKind.html)..ord(VNodeKind.high):
31 | # +1 because of empty node at the start of the enum AST:
32 | let tag = $x[i+1]
33 | allcomponents[tag] = ComponentKind.Tag
34 |
35 | static:
36 | addTags()
37 |
38 | proc unpack(symbolicType: NimNode; index: int): NimNode {.compileTime.} =
39 | #let t = symbolicType.getTypeImpl
40 | let t = repr(symbolicType)
41 | case t
42 | of "cstring":
43 | result = quote do:
44 | args[`index`].text
45 | of "int", "VKey":
46 | result = quote do:
47 | args[`index`].intValue
48 | of "bool":
49 | result = quote do:
50 | args[`index`].intValue != 0
51 | elif t.endsWith"Kind":
52 | result = quote do:
53 | `symbolicType`(args[`index`].intValue)
54 | else:
55 | # just pass it along, maybe there is some conversion for it:
56 | result = quote do:
57 | args[`index`]
58 |
59 | proc newname*(n: NimNode): NimNode =
60 | if n.kind == nnkPostfix:
61 | n[1] = newname(n[1])
62 | result = n
63 | elif n.kind == nnkSym:
64 | result = ident(n.strVal)
65 | else:
66 | result = n
67 |
68 | when defined(js):
69 | macro compact*(prc: untyped): untyped =
70 | ## A 'compact' tree generation proc is one that only depends on its
71 | ## inputs and should be stored as a compact virtual DOM tree and
72 | ## only expanded on demand (when its inputs changed).
73 | var n = prc.copyNimNode
74 | for i in 0..6: n.add prc[i].copyNimTree
75 | expectKind(n, nnkProcDef)
76 | if n[0].kind == nnkEmpty:
77 | error("please pass a non anonymous proc", n[0])
78 | let name = n[0]
79 | let params = params(n)
80 | let rettype = repr params[0]
81 | var isvirtual = ComponentKind.None
82 | if rettype == "VNode":
83 | isvirtual = ComponentKind.VNode
84 | else:
85 | error "component must return VNode", params[0]
86 | let realName = if name.kind == nnkPostfix: name[1] else: name
87 | let nn = $realName
88 | n[0] = ident("inner" & nn)
89 | var unpackCall = newCall(n[0])
90 | var counter = 0
91 | for i in 1 ..< params.len:
92 | let param = params[i]
93 | let L = param.len
94 | let typ = param[L-2]
95 | for j in 0 .. L-3:
96 | unpackCall.add unpack(typ, counter)
97 | inc counter
98 |
99 | template vwrapper(pname, unpackCall) {.dirty.} =
100 | proc pname(args: seq[VNode]): VNode =
101 | unpackCall
102 |
103 | template vregister(key, val) =
104 | bind jdict.`[]=`
105 | `[]=`(vcomponents, kstring(key), val)
106 |
107 | result = newTree(nnkStmtList, n)
108 |
109 | if isvirtual == ComponentKind.VNode:
110 | result.add getAst(vwrapper(newname name, unpackCall))
111 | result.add getAst(vregister(newLit(nn), realName))
112 | allcomponents[nn] = isvirtual
113 | when defined(debugKaraxDsl):
114 | echo repr result
115 |
--------------------------------------------------------------------------------
/karax/errors.nim:
--------------------------------------------------------------------------------
1 | ## Error handling logic for form input validation.
2 |
3 | import karax, jdict
4 |
5 | var
6 | gerrorMsgs = newJDict[cstring, cstring]()
7 | gerrorCounter = 0
8 |
9 | proc hasErrors*(): bool = gerrorCounter != 0
10 |
11 | proc hasError*(field: cstring): bool = gerrorMsgs.contains(field) and len(gerrorMsgs[field]) > 0
12 |
13 | proc disableOnError*(): cstring = toDisabled(hasErrors())
14 |
15 | proc getError*(field: cstring): cstring =
16 | if not gerrorMsgs.contains(field):
17 | result = ""
18 | else:
19 | result = gerrorMsgs[field]
20 |
21 | proc setError*(field, msg: cstring) =
22 | let previous = getError(field)
23 | if len(msg) == 0:
24 | if len(previous) != 0: dec(gerrorCounter)
25 | else:
26 | if len(previous) == 0: inc(gerrorCounter)
27 | gerrorMsgs[field] = msg
28 |
29 | proc clearErrors*() =
30 | let m = gerrorMsgs
31 | for k in m.keys:
32 | setError(k, "")
33 |
--------------------------------------------------------------------------------
/karax/i18n.nim:
--------------------------------------------------------------------------------
1 | ## Karax -- Single page applications for Nim.
2 |
3 | ## i18n support for Karax applications. We distinguish between:
4 | ##
5 | ## - translate: Lookup text translations
6 | ## - localize: Localize Date and Time objects to local formats
7 | ##
8 | ## Localization has not yet been implemented.
9 |
10 | import languages, jdict, jstrutils
11 |
12 | var currentLanguage = Language.enUS
13 |
14 | proc getLanguage(): cstring =
15 | {.emit: "`result` = (navigator.languages && navigator.languages.length) ? navigator.languages[0] : navigator.language;".}
16 |
17 | proc detectLanguage*(): Language =
18 | let x = getLanguage()
19 | for i in low(Language)..high(Language):
20 | if languageToCode[i] == x: return i
21 | return Language.enUS
22 |
23 | proc setCurrentLanguage*(newLanguage = detectLanguage()) =
24 | currentLanguage = newLanguage
25 |
26 | proc getCurrentLanguage*(): Language = currentLanguage
27 |
28 | type Translation* = JDict[cstring, cstring]
29 |
30 | var
31 | translations: array[Language, Translation]
32 |
33 | proc registerTranslation*(lang: Language; t: Translation) = translations[lang] = t
34 |
35 | proc addTranslation*(lang: Language; key, val: cstring) =
36 | if translations[lang].isNil: translations[lang] = newJDict[cstring, cstring]()
37 | translations[lang][key] = val
38 |
39 | proc translate(x: cstring): cstring =
40 | let y = translations[currentLanguage]
41 | if y != nil and y.contains(x):
42 | result = y[x]
43 | else:
44 | result = x
45 |
46 | type TranslatedString* = distinct cstring
47 |
48 | template i18n*(x: string): TranslatedString = TranslatedString(translate(cstring x))
49 |
50 | proc raiseInvalidFormat(errmsg: string) =
51 | raise newException(ValueError, errmsg)
52 |
53 | discard """
54 | $[1:item|items]1$ selected.
55 | """
56 |
57 | proc `{}`(x: cstring; i: int): char {.importcpp: "#.charCodeAt(#)".}
58 | proc sadd(s: JSeq[char]; c: char) {.importcpp: "#.push(String.fromCharCode(#))".}
59 |
60 | proc parseChoice(f: cstring; i, choice: int,
61 | r: JSeq[char]) =
62 | var i = i
63 | while i < f.len:
64 | var n = 0
65 | let oldI = i
66 | var toAdd = false
67 | while i < f.len and f{i} >= '0' and f{i} <= '9':
68 | n = n * 10 + ord(f{i}) - ord('0')
69 | inc i
70 | if oldI != i:
71 | if f{i} == ':':
72 | inc i
73 | else:
74 | raiseInvalidFormat"':' after number expected"
75 | toAdd = choice == n
76 | else:
77 | # an else section does not start with a number:
78 | toAdd = true
79 | while i < f.len and f{i} != ']' and f{i} != '|':
80 | if toAdd: r.sadd f{i}
81 | inc i
82 | if toAdd: break
83 | inc i
84 |
85 | proc join(x: JSeq[char]): cstring {.importcpp: "#.join(\"\")".}
86 | proc add(x: JSeq[char]; y: cstring) =
87 | for i in 0..= '0' and f{i} <= '9':
107 | j = j * 10 + ord(f{i}) - ord('0')
108 | inc i
109 | let idx = if not negative: j-1 else: args.len-j
110 | r.add args[idx]
111 | of '$':
112 | inc(i)
113 | r.add '$'
114 | of '[':
115 | let start = i+1
116 | while i < f.len and f{i} != ']': inc i
117 | inc i
118 | if i >= f.len: raiseInvalidFormat"']' expected"
119 | case f{i}
120 | of '#':
121 | parseChoice(f, start, parseInt args[num], r)
122 | inc i
123 | inc num
124 | of '1'..'9', '-':
125 | var j = 0
126 | var negative = f{i} == '-'
127 | if negative: inc i
128 | while f{i} >= '0' and f{i} <= '9':
129 | j = j * 10 + ord(f{i}) - ord('0')
130 | inc i
131 | let idx = if not negative: j-1 else: args.len-j
132 | parseChoice(f, start, parseInt args[idx], r)
133 | else: raiseInvalidFormat"argument index expected after ']'"
134 | else:
135 | raiseInvalidFormat("'#', '$', or number expected")
136 | if i < f.len and f{i} == '$': inc i
137 | else:
138 | r.sadd f{i}
139 | inc i
140 | result = join(r)
141 |
142 |
143 | when isMainModule:
144 | addTranslation(Language.deDE, "$#$ -> $#$", "$2 macht $1")
145 | setCurrentLanguage(Language.deDE)
146 |
147 | echo(i18n"$#$ -> $#$" % [cstring"1", "2"])
148 | echo(i18n"$[1:item |items ]1$ -> $1" % [cstring"1", "2"])
149 | echo(i18n"$[1:item |items ]1 -> $1" % [cstring"0", "2"])
150 | echo(i18n"$[1:item |items ]1 -> $1" % [cstring"2", "2"])
151 | echo(i18n"$[1:item |items ]1$ -> $1" % [cstring"3", "2"])
152 | echo(i18n"$1 $[1:item |4:huha |items ]1$ -> $1" % [cstring"4", "2"])
153 |
--------------------------------------------------------------------------------
/karax/jdict.nim:
--------------------------------------------------------------------------------
1 |
2 | type
3 | JDict*[K, V] = ref object
4 |
5 | proc `[]`*[K, V](d: JDict[K, V], k: K): V {.importcpp: "#[#]".}
6 | proc `[]=`*[K, V](d: JDict[K, V], k: K, v: V) {.importcpp: "#[#] = #".}
7 |
8 | proc newJDict*[K, V](): JDict[K, V] {.importcpp: "{@}".}
9 |
10 | proc contains*[K, V](d: JDict[K, V], k: K): bool {.importcpp: "#.hasOwnProperty(#)".}
11 |
12 | proc del*[K, V](d: JDict[K, V], k: K) {.importcpp: "delete #[#]".}
13 |
14 | iterator keys*[K, V](d: JDict[K, V]): K =
15 | var kkk: K
16 | {.emit: ["for (", kkk, " in ", d, ") {"].}
17 | yield kkk
18 | {.emit: ["}"].}
19 |
20 | type
21 | JSeq*[T] = ref object
22 |
23 | proc `[]`*[T](s: JSeq[T], i: int): T {.importcpp: "#[#]", noSideEffect.}
24 | proc `[]=`*[T](s: JSeq[T], i: int, v: T) {.importcpp: "#[#] = #", noSideEffect.}
25 |
26 | proc newJSeq*[T](len: int = 0): JSeq[T] {.importcpp: "new Array(#)".}
27 | proc len*[T](s: JSeq[T]): int {.importcpp: "#.length", noSideEffect.}
28 | proc add*[T](s: JSeq[T]; x: T) {.importcpp: "#.push(#)", noSideEffect.}
29 |
30 | proc shrink*[T](s: JSeq[T]; shorterLen: int) {.importcpp: "#.length = #", noSideEffect.}
31 |
--------------------------------------------------------------------------------
/karax/jjson.nim:
--------------------------------------------------------------------------------
1 | ## This module implements some small zero-overhead 'JsonNode' type
2 | ## and helper that maps directly to JavaScript objects.
3 |
4 | type
5 | JsonNode* {.importc.} = ref object
6 |
7 | proc `[]`*(obj: JsonNode; fieldname: cstring): JsonNode {.importcpp: "#[#]".}
8 | proc `[]`*(obj: JsonNode; index: int): JsonNode {.importcpp: "#[#]".}
9 | proc `[]=`*[T](obj: JsonNode; fieldname: cstring; value: T)
10 | {.importcpp: "#[#] = #".}
11 | proc length(x: JsonNode): int {.importcpp: "#.length".}
12 | proc len*(x: JsonNode): int = (if x.isNil: 0 else: x.length)
13 |
14 | proc parse*(input: cstring): JsonNode {.importcpp: "JSON.parse(#)".}
15 | proc hasField*(obj: JsonNode; fieldname: cstring): bool {.importcpp: "#[#] !== undefined".}
16 |
17 | proc newJsonNode*(fields: varargs[(cstring, JsonNode)]): JsonNode =
18 | result = JsonNode()
19 | for f in fields:
20 | result[f[0]] = f[1]
21 |
22 | proc newJObject*(): JsonNode =
23 | result = JsonNode()
24 |
25 | proc newJArray*(elements: varargs[JsonNode]): JsonNode {.importcpp: "#".}
26 |
27 | proc newJNull*(): JsonNode = nil
28 |
29 | template `%`*(x: typed): JsonNode = cast[JsonNode](x)
30 | template `%`*(x: string): JsonNode = cast[JsonNode](cstring x)
31 |
32 | proc getNum*(x: JsonNode): int {.importcpp: "#".}
33 |
34 | proc getInt*(x: JsonNode): int {.importcpp: "#".}
35 | proc getStr*(x: JsonNode): cstring {.importcpp: "#".}
36 | proc getFNum*(x: JsonNode): cstring {.importcpp: "#".}
37 | proc getBool*(x: JsonNode): bool {.importcpp: "#".}
38 |
39 | iterator items*(x: JsonNode): JsonNode =
40 | for i in 0..=0)", nodecl.}
11 |
12 | proc containsIgnoreCase*(a, b: cstring): bool {.
13 | importcpp: """(#.search(new RegExp(#.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$$\|]/g, "\\$$&") , "i"))>=0)""", nodecl.}
14 |
15 | proc substr*(s: cstring; start: int): cstring {.importcpp: "substr", nodecl.}
16 | proc substr*(s: cstring; start, length: int): cstring {.importcpp: "substr", nodecl.}
17 |
18 | #proc len*(s: cstring): int {.importcpp: "#.length", nodecl.}
19 | proc `&`*(a, b: cstring): cstring {.importcpp: "(# + #)", nodecl.}
20 | proc toCstr*(s: int): cstring {.importcpp: "((#)+'')", nodecl.}
21 | proc `&`*(s: int): cstring {.importcpp: "((#)+'')", nodecl.}
22 | proc `&`*(s: bool): cstring {.importcpp: "((#)+'')", nodecl.}
23 | proc `&`*(s: float): cstring {.importcpp: "((#)+'')", nodecl.}
24 |
25 | proc `&`*(s: cstring): cstring {.importcpp: "(#)", nodecl.}
26 |
27 | proc isInt*(s: cstring): bool {.asmNoStackFrame.} =
28 | asm """
29 | return `s`.match(/^[0-9]+$/);
30 | """
31 |
32 | proc parseInt*(s: cstring): int {.importcpp: "parseInt(#, 10)", nodecl.}
33 | proc parseFloat*(s: cstring): BiggestFloat {.importc, nodecl.}
34 |
35 | proc join*(a: openArray[cstring], sep = cstring""): cstring {.importcpp: "(#.join(#))", nodecl.}
36 |
--------------------------------------------------------------------------------
/karax/jwebsockets.nim:
--------------------------------------------------------------------------------
1 | ## Websockets support for the JSON backend.
2 |
3 | type
4 | MessageEvent* {.importc.} = ref object
5 | data*: cstring
6 |
7 | WebSocket* {.importc.} = ref object
8 | onmessage*: proc (e: MessageEvent)
9 | onopen*: proc (e: MessageEvent)
10 |
11 | proc newWebSocket*(url, key: cstring): WebSocket
12 | {.importcpp: "new WebSocket(@)".}
13 |
14 | proc newWebSocket*(url: cstring): WebSocket
15 | {.importcpp: "new WebSocket(@)".}
16 |
17 | proc send*(w: WebSocket; data: cstring) {.importcpp.}
18 |
--------------------------------------------------------------------------------
/karax/kajax.nim:
--------------------------------------------------------------------------------
1 | ## Karax -- Single page applications for Nim.
2 |
3 | ## This module implements support for `ajax`:idx: socket
4 | ## handling.
5 |
6 | import karax
7 | import jsffi except `&`
8 | import jscore
9 | import dom
10 |
11 | type
12 | ProgressEvent* {.importc.}= object
13 | loaded*: float
14 | total*: float
15 |
16 | FormData* {.importc.} = JsObject
17 |
18 | HttpRequest* {.importc.} = ref object
19 | readyState, status: int
20 | responseText, statusText: cstring
21 |
22 | XMLHttpRequestUpload* {.importc.} = JsObject
23 |
24 | proc newFormData*(): FormData {.importcpp: "new FormData()".}
25 | proc append*(f: FormData, key: cstring, value: Blob) {.importcpp:"#.append(@)".}
26 | proc append*(f: FormData, key: cstring, value: cstring) {.importcpp:"#.append(@)".}
27 |
28 | proc setRequestHeader*(r: HttpRequest; a, b: cstring) {.importcpp: "#.setRequestHeader(@)".}
29 | proc statechange*(r: HttpRequest; cb: proc()) {.importcpp: "#.onreadystatechange = #".}
30 | proc send*(r: HttpRequest; data: cstring) {.importcpp: "#.send(#)".}
31 | proc send*(r: HttpRequest, data: Blob) {.importcpp: "#.send(#)".}
32 | proc open*(r: HttpRequest; meth, url: cstring; async: bool) {.importcpp: "#.open(@)".}
33 | proc newRequest*(): HttpRequest {.importcpp: "new XMLHttpRequest(@)".}
34 |
35 | when not declared(dom.File):
36 | type
37 | DomFile = ref FileObj
38 | FileObj {.importc.} = object of Blob
39 | lastModified: int
40 | name: cstring
41 | else:
42 | type
43 | DomFile = dom.File
44 |
45 | proc uploadFile*(url: cstring, file: Blob, onprogress :proc(data: ProgressEvent),
46 | cont: proc (httpStatus: int; response: cstring);
47 | headers: openarray[(cstring, cstring)] = []) =
48 | proc contWrapper(httpStatus: int; response: cstring) =
49 | cont(httpStatus, response)
50 |
51 | proc upload(r: HttpRequest):XMLHttpRequestUpload {.importcpp: "#.upload".}
52 |
53 | var formData = newFormData()
54 | formData.append("upload_file",file)
55 | formData.append("filename", DomFile(file).name)
56 | let ajax = newRequest()
57 | ajax.open("POST", url, true)
58 | for a, b in items(headers):
59 | ajax.setRequestHeader(a, b)
60 | ajax.statechange proc() =
61 | if ajax.readyState == 4:
62 | contWrapper(ajax.status, ajax.responseText)
63 | ajax.upload.onprogress = onprogress
64 | ajax.send(formData.to(cstring))
65 |
66 | proc ajax*(meth, url: cstring; headers: openarray[(cstring, cstring)];
67 | data: cstring;
68 | cont: proc (httpStatus: int; response: cstring);
69 | doRedraw: bool = true,
70 | kxi: KaraxInstance = kxi,
71 | useBinary: bool = false,
72 | blob: Blob = nil) =
73 | proc contWrapper(httpStatus: int; response: cstring) =
74 | cont(httpStatus, response)
75 | if doRedraw: redraw(kxi)
76 |
77 |
78 | let ajax = newRequest()
79 | ajax.open(meth, url, true)
80 | for a, b in items(headers):
81 | ajax.setRequestHeader(a, b)
82 | ajax.statechange proc() =
83 | if ajax.readyState == 4:
84 | if ajax.status == 200:
85 | contWrapper(ajax.status, ajax.responseText)
86 | else:
87 | contWrapper(ajax.status, ajax.responseText)
88 | if useBinary:
89 | ajax.send(blob)
90 | else:
91 | ajax.send(data)
92 |
93 | proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)];
94 | data: cstring;
95 | cont: proc (httpStatus: int, response: cstring);
96 | doRedraw: bool = true,
97 | kxi: KaraxInstance = kxi) =
98 | ajax("POST", url, headers, data, cont, doRedraw, kxi)
99 |
100 | proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)];
101 | data: Blob;
102 | cont: proc (httpStatus: int, response: cstring);
103 | doRedraw: bool = true,
104 | kxi: KaraxInstance = kxi) =
105 | ajax("POST", url, headers, "", cont, doRedraw, kxi, true, data)
106 |
107 | proc ajaxGet*(url: cstring; headers: openarray[(cstring, cstring)];
108 | cont: proc (httpStatus: int, response: cstring);
109 | doRedraw: bool = true,
110 | kxi: KaraxInstance = kxi) =
111 | ajax("GET", url, headers, nil, cont, doRedraw, kxi)
112 |
113 | proc ajaxPut*(url: cstring; headers: openarray[(cstring, cstring)];
114 | data: cstring;
115 | cont: proc (httpStatus: int, response: cstring);
116 | doRedraw: bool = true,
117 | kxi: KaraxInstance = kxi) =
118 | ajax("PUT", url, headers, data, cont, doRedraw, kxi)
119 |
120 | proc ajaxDelete*(url: cstring; headers: openarray[(cstring, cstring)];
121 | cont: proc (httpStatus: int, response: cstring);
122 | doRedraw: bool = true,
123 | kxi: KaraxInstance = kxi) =
124 | ajax("DELETE", url, headers, nil, cont, doRedraw, kxi)
125 |
126 |
127 | proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".}
128 | proc fromJson*[T](blob: cstring): T {.importc: "JSON.parse".}
129 |
130 |
--------------------------------------------------------------------------------
/karax/karaxdsl.nim:
--------------------------------------------------------------------------------
1 |
2 | import macros, vdom, compact, kbase
3 | from strutils import startsWith, toLowerAscii, cmpIgnoreStyle
4 |
5 | when defined(js):
6 | import karax
7 |
8 | const
9 | StmtContext = ["kout", "inc", "echo", "dec", "!"]
10 | SpecialAttrs = ["id", "class", "value", "index", "style"]
11 |
12 | proc getName(n: NimNode): string =
13 | case n.kind
14 | of nnkIdent, nnkSym:
15 | result = $n
16 | of nnkAccQuoted:
17 | result = ""
18 | for i in 0.. 0:
50 | var hasNoRedrawPragma = false
51 | for i in 0 ..< len(anon.pragma):
52 | # using anon because anon needs to get rid of the pragma
53 | if anon.pragma[i].kind == nnkIdent and cmpIgnoreStyle(anon.pragma[i].strVal, "noredraw") == 0:
54 | hasNoRedrawPragma = true
55 | anon.pragma.del(i)
56 | break
57 | if hasNoRedrawPragma:
58 | return newCall(ident"addEventHandlerNoRedraw", tmpContext,
59 | newDotExpr(bindSym"EventKind", name), anon)
60 | result = call
61 |
62 | proc tcall2(n, tmpContext: NimNode): NimNode =
63 | # we need to distinguish statement and expression contexts:
64 | # every call statement 's' needs to be transformed to 'dest.add s'.
65 | # If expressions need to be distinguished from if statements. Since
66 | # we know we start in a statement context, it's pretty simple to
67 | # figure out expression contexts: In calls everything is an expression
68 | # (except for the last child of the macros we consider here),
69 | # lets, consts, types can be considered as expressions
70 | # case is complex, calls are assumed to produce a value.
71 | when defined(js):
72 | template evHandler(): untyped = bindSym"addEventHandler"
73 | else:
74 | template evHandler(): untyped = ident"addEventHandler"
75 |
76 | case n.kind
77 | of nnkLiterals, nnkIdent, nnkSym, nnkDotExpr, nnkBracketExpr:
78 | if tmpContext != nil:
79 | result = newCall(bindSym"add", tmpContext, n)
80 | else:
81 | result = n
82 | of nnkForStmt, nnkIfExpr, nnkElifExpr, nnkElseExpr,
83 | nnkOfBranch, nnkElifBranch, nnkExceptBranch, nnkElse,
84 | nnkConstDef, nnkWhileStmt, nnkIdentDefs, nnkVarTuple:
85 | # recurse for the last son:
86 | result = copyNimTree(n)
87 | let L = n.len
88 | assert n.len == result.len
89 | if L > 0:
90 | result[L-1] = tcall2(result[L-1], tmpContext)
91 | of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkTryStmt,
92 | nnkFinally, nnkBlockStmt, nnkBlockExpr:
93 | # recurse for every child:
94 | result = copyNimNode(n)
95 | for x in n:
96 | result.add tcall2(x, tmpContext)
97 | of nnkCaseStmt:
98 | # recurse for children, but don't add call for case ident
99 | result = copyNimNode(n)
100 | result.add n[0]
101 | for i in 1 ..< n.len:
102 | result.add tcall2(n[i], tmpContext)
103 | of nnkProcDef:
104 | let name = getName n[0]
105 | if name.startsWith"on":
106 | # turn it into an anon proc:
107 | let anon = copyNimTree(n)
108 | anon[0] = newEmptyNode()
109 | if tmpContext == nil:
110 | error "no VNode to attach the event handler to"
111 | else:
112 | let call = newCall(evHandler(), tmpContext,
113 | newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi"))
114 | result = handleNoRedrawPragma(call, tmpContext, n[0], anon)
115 | else:
116 | result = n
117 | of nnkVarSection, nnkLetSection, nnkConstSection:
118 | result = n
119 | of nnkCallKinds - {nnkInfix}:
120 | let op = getName(n[0])
121 | let ck = isComponent(op)
122 | if ck != ComponentKind.None:
123 | let tmp = genSym(nskLet, "tmp")
124 | let call = if ck == ComponentKind.Tag:
125 | newCall(bindSym"tree", newDotExpr(bindSym"VNodeKind", n[0]))
126 | elif ck == ComponentKind.VNode:
127 | newCall(bindSym"vthunk", newLit(op))
128 | else:
129 | newCall(bindSym"dthunk", newLit(op))
130 | result = newTree(
131 | if tmpContext == nil: nnkStmtListExpr else: nnkStmtList,
132 | newLetStmt(tmp, call))
133 | for i in 1 ..< n.len:
134 | # named parameters are transformed into attributes or events:
135 | let x = n[i]
136 | if x.kind == nnkExprEqExpr:
137 | let key = getName x[0]
138 | if key.startsWith("on"):
139 | result.add newCall(evHandler(),
140 | tmp, newDotExpr(bindSym"EventKind", x[0]), x[1], ident("kxi"))
141 | elif eqIdent(key, "style") and x[1].kind == nnkTableConstr:
142 | result.add newDotAsgn(tmp, key, newCall("style", toKstring x[1]))
143 | elif key in SpecialAttrs:
144 | result.add newDotAsgn(tmp, key, x[1])
145 | if key == "value":
146 | result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
147 | elif eqIdent(key, "setFocus"):
148 | result.add newCall(key, tmp, x[1], ident"kxi")
149 | elif eqIdent(key, "events"):
150 | result.add newCall(bindSym"mergeEvents", tmp, x[1])
151 | else:
152 | result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
153 | elif ck != ComponentKind.Tag:
154 | call.add x
155 | elif eqIdent(x, "setFocus"):
156 | result.add newCall(x, tmp, bindSym"true", ident"kxi")
157 | else:
158 | result.add tcall2(x, tmp)
159 | if tmpContext == nil:
160 | result.add tmp
161 | else:
162 | result.add newCall(bindSym"add", tmpContext, tmp)
163 | elif tmpContext != nil and op notin StmtContext:
164 | var hasEventHandlers = false
165 | for i in 1..= 0:
205 | x.s.delete(position)
206 | x.broadcast(Mark)
207 | x.broadcast(Deleted, position)
208 |
209 | when false:
210 | proc `:=`[T](x: Reactive[T], f: proc(): T) =
211 | toTrack = wrapObserver(f())
212 | x.value = f()
213 | toTrack = nil
214 |
215 | proc map*[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] =
216 | let xl = x.L.value
217 | let res = newRSeq[U](xl)
218 | for i in 0..
7 |
8 | """
9 |
10 | html = """
11 |
12 |
13 |
14 |
15 |
16 | $1
17 |
18 | $2
19 |
20 |
21 |
$3
22 | $4
23 |
24 |
25 | """
26 |
27 | websocket = """
28 |
47 | """
48 |
49 | proc exec(cmd: string) =
50 | if os.execShellCmd(cmd) != 0:
51 | quit "External command failed: " & cmd
52 |
53 | proc build(ssr: bool, entry: string, rest: string, selectedCss: string, run: bool, watch: bool) =
54 | echo("Building...")
55 | var cmd: string
56 | var content = ""
57 | var outTempPath: string
58 | var outHtmlName: string
59 | if ssr:
60 | outHtmlName = changeFileExt(extractFilename(entry),"html")
61 | outTempPath = getTempDir() / outHtmlName
62 | cmd = "nim c -r " & rest & " " & outTempPath
63 | else:
64 | cmd = "nim js --out:" & "app" & ".js " & rest
65 | if watch:
66 | discard os.execShellCmd(cmd)
67 | else:
68 | exec cmd
69 | let dest = "app" & ".html"
70 | let script = if ssr:"" else: """""" & (if watch: websocket else: "")
71 | if ssr:
72 | content = readFile(outTempPath)
73 | writeFile(dest, html % [if ssr: outHtmlName else:"app", selectedCss,content, script])
74 | if run: openDefaultBrowser("http://localhost:8080")
75 |
76 | proc watchBuild(ssr: bool, filePath: string, selectedCss: string, rest: string) {.thread.} =
77 | var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
78 | while true:
79 | sleep(300)
80 | for path in walkDirRec("."):
81 | if ".git" in path:
82 | continue
83 | var (_, _, ext) = splitFile(path)
84 | if ext in [".scss", ".sass", ".less", ".styl", ".pcss", ".postcss"]:
85 | continue
86 | if files.hasKey(path):
87 | if files[path] != getLastModificationTime(path):
88 | echo("File changed: " & path)
89 | build(ssr, filePath, rest,selectedCss, false, true)
90 | files[path] = getLastModificationTime(path)
91 | else:
92 | if absolutePath(path) in [absolutePath("app" & ".js"), absolutePath("app" & ".html")]:
93 | continue
94 | files[path] = getLastModificationTime(path)
95 |
96 | proc serve() {.thread.} =
97 | serveStatic()
98 |
99 | proc main =
100 | var op = initOptParser()
101 | var rest = op.cmdLineRest
102 | var file = ""
103 | var run = false
104 | var watch = false
105 | var selectedCss = ""
106 | var ssr = false
107 | while true:
108 | op.next()
109 | case op.kind
110 | of cmdLongOption:
111 | case op.key
112 | of "run":
113 | run = true
114 | rest = rest.replace("--run ")
115 | of "css":
116 | if op.val != "":
117 | selectedCss = readFile(op.val)
118 | else:
119 | selectedCss = css
120 | rest = rest.substr(rest.find(" "))
121 | of "ssr":
122 | ssr = true
123 | rest = rest.replace("--ssr ")
124 | else: discard
125 | of cmdShortOption:
126 | if op.key == "r":
127 | run = true
128 | rest = rest.replace("-r ")
129 | if op.key == "w":
130 | watch = true
131 | rest = rest.replace("-w ")
132 | if op.key == "s":
133 | ssr = true
134 | rest = rest.replace("-s ")
135 | of cmdArgument: file = op.key
136 | of cmdEnd: break
137 |
138 | if file.len == 0: quit "filename expected"
139 | if run:
140 | spawn serve()
141 | if watch:
142 | spawn watchBuild(ssr, file, selectedCss, rest)
143 | build(ssr, file, rest, selectedCss, run, watch)
144 | sync()
145 |
146 | main()
147 |
--------------------------------------------------------------------------------
/karax/tools/karun.nims:
--------------------------------------------------------------------------------
1 | switch("threads", "on")
--------------------------------------------------------------------------------
/karax/tools/static_server.nim:
--------------------------------------------------------------------------------
1 | import
2 | std/[net, os, strutils, uri, mimetypes, asyncnet, asyncdispatch, md5,
3 | logging, httpcore, asyncfile, asynchttpserver, tables, times]
4 |
5 | import ws, dotenv
6 |
7 | var logger = newConsoleLogger()
8 | addHandler(logger)
9 |
10 | when defined(release):
11 | setLogFilter(lvlError)
12 |
13 | type
14 | RawHeaders* = seq[tuple[key, val: string]]
15 |
16 | proc toStr(headers: RawHeaders): string =
17 | $newHttpHeaders(headers)
18 |
19 | proc send(request: Request, code: HttpCode, headers: RawHeaders,
20 | body: string): Future[void] =
21 | return request.respond(code, body, newHttpHeaders(headers))
22 |
23 | proc statusContent(request: Request, status: HttpCode, content: string,
24 | headers: RawHeaders): Future[void] =
25 | try:
26 | result = send(request, status, headers, content)
27 | debug(" ", status, " ", toStr(headers))
28 | except:
29 | error("Could not send response: ", osErrorMsg(osLastError()))
30 |
31 | proc sendStaticIfExists(req: Request, paths: seq[string]): Future[HttpCode] {.async.} =
32 | result = Http200
33 | let mimes = newMimetypes()
34 | for p in paths:
35 | if fileExists(p):
36 | if fpOthersRead notin getFilePermissions(p):
37 | return Http403
38 | let fileSize = getFileSize(p)
39 | let extPos = searchExtPos(p)
40 | let mimetype = mimes.getMimetype(
41 | if extPos >= 0: p.substr(extPos + 1)
42 | else: "")
43 | if fileSize < 10_000_000: # 10 mb
44 | var file = readFile(p)
45 | var hashed = getMD5(file)
46 | # If the user has a cached version of this file and it matches our
47 | # version, let them use it
48 | if req.headers.getOrDefault("If-None-Match") == hashed:
49 | await req.statusContent(Http304, "", default(RawHeaders))
50 | else:
51 | await req.statusContent(Http200, file, @{
52 | "Content-Type": mimetype,
53 | "ETag": hashed
54 | })
55 | else:
56 | let headers = @{
57 | "Content-Type": mimetype,
58 | "Content-Length": $fileSize
59 | }
60 | await req.statusContent(Http200, "", headers)
61 | var fileStream = newFutureStream[string]("sendStaticIfExists")
62 | var file = openAsync(p, fmRead)
63 | # Let `readToStream` write file data into fileStream in the
64 | # background.
65 | asyncCheck file.readToStream(fileStream)
66 | # The `writeFromStream` proc will complete once all the data in the
67 | # `bodyStream` has been written to the file.
68 | while true:
69 | let (hasValue, value) = await fileStream.read()
70 | if hasValue:
71 | await req.client.send(value)
72 | else:
73 | break
74 | file.close()
75 | return
76 | # If we get to here then no match could be found.
77 | return Http404
78 |
79 | proc handleFileRequest(req: Request): Future[HttpCode] {.async.} =
80 | # Find static file.
81 | var reqPath = decodeUrl(req.url.path)
82 | var staticDir = getEnv("staticDir") # it's assumed a relative dir
83 | var status = Http400
84 | var path = staticDir / reqPath
85 | normalizePathEnd(path, false)
86 | if dirExists(path):
87 | status = await sendStaticIfExists(req, @[path / "index.html", path / "index.htm"])
88 | else:
89 | status = await sendStaticIfExists(req, @[path])
90 | return status
91 |
92 | proc handleWs(req: Request) {.async.} =
93 | var ws = await newWebSocket(req)
94 | await ws.send("Welcome to simple echo server")
95 |
96 | var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
97 | let watchedFiles = [absolutePath "app.js", absolutePath "app.html"]
98 | for path in watchedFiles:
99 | files[path] = getLastModificationTime(path)
100 |
101 | while ws.readyState == Open:
102 | await sleepAsync(500)
103 | var changed = false
104 | for path in watchedFiles:
105 | if files[path] != getLastModificationTime(path):
106 | changed = true
107 | files[path] = getLastModificationTime(path)
108 | if changed:
109 | await ws.send("refresh")
110 | changed = false
111 |
112 | proc serveStatic*() =
113 | if fileExists("static.env"):
114 | overload(getCurrentDir(), "static.env")
115 | else:
116 | putEnv("staticDir", "assets/")
117 |
118 | var server = newAsyncHttpServer()
119 | proc cb(req: Request) {.gcsafe, async.} =
120 | if req.url.path == "/ws":
121 | await handleWs(req)
122 | if req.url.path == "/":
123 | await req.respond(Http200, readFile "app.html")
124 | elif req.url.path == "/app.js":
125 | let file = absolutePath("app" & ".js")
126 | if not file.fileExists:
127 | error(file, " does not exist!")
128 | if fpUserRead notin os.getFilePermissions(file):
129 | error("Could not read ", file, "!")
130 | await req.respond(Http200, readFile(file))
131 | else:
132 | let status = await handleFileRequest(req)
133 | if status != Http200:
134 | await req.respond(status, "")
135 |
136 | waitFor server.serve(Port(8080), cb)
137 |
138 | when isMainModule:
139 | serveStatic()
140 |
--------------------------------------------------------------------------------
/karax/vdom.nim:
--------------------------------------------------------------------------------
1 | ## Virtual DOM implementation for Karax.
2 |
3 | when defined(js):
4 | from kdom import Event, Node
5 | else:
6 | type
7 | Event* = ref object
8 | Node* = ref object
9 |
10 | import macros, vstyles, kbase
11 | from strutils import toUpperAscii, toLowerAscii, tokenize
12 |
13 | type
14 | VNodeKind* {.pure.} = enum
15 | text = "#text", int = "#int", bool = "#bool",
16 | vthunk = "#vthunk", dthunk = "#dthunk",
17 | component = "#component", verbatim = "#verbatim",
18 |
19 | html, head, title, base, link, meta, style,
20 | script, noscript,
21 | body, section, nav, article, aside,
22 | h1, h2, h3, h4, h5, h6, hgroup,
23 | header, footer, address, main,
24 |
25 | p, hr, pre, blockquote, ol, ul, li,
26 | dl, dt, dd,
27 | figure, figcaption,
28 |
29 | tdiv = "div",
30 |
31 | a, em, strong, small,
32 | strikethrough = "s", cite, quote,
33 | dfn, abbr, data, time, code, `var` = "var", samp,
34 | kbd, sub, sup, italic = "i", bold = "b", underlined = "u",
35 | mark, ruby, rt, rp, bdi, dbo, span, br, wbr,
36 | ins, del, img, iframe, embed, `object` = "object",
37 | param, video, audio, source, track, canvas, map, area,
38 |
39 | # SVG elements, see: https://www.w3.org/TR/SVG2/eltindex.html
40 | animate, animateMotion, animateTransform, circle, clipPath, defs, desc,
41 | `discard` = "discard", ellipse, feBlend, feColorMatrix, feComponentTransfer,
42 | feComposite, feConvolveMatrix, feDiffuseLighting, feDisplacementMap,
43 | feDistantLight, feDropShadow, feFlood, feFuncA, feFuncB, feFuncG, feFuncR,
44 | feGaussianBlur, feImage, feMerge, feMergeNode, feMorphology, feOffset,
45 | fePointLight, feSpecularLighting, feSpotLight, feTile, feTurbulence,
46 | filter, foreignObject, g, image, line, linearGradient, marker, mask,
47 | metadata, mpath, path, pattern, polygon, polyline, radialGradient, rect,
48 | `set` = "set", stop, svg, switch, symbol, stext = "text", textPath, tspan,
49 | unknown, use, view,
50 |
51 | # MathML elements
52 | maction, math, menclose, merror, mfenced, mfrac, mglyph, mi, mlabeledtr,
53 | mmultiscripts, mn, mo, mover, mpadded, mphantom, mroot, mrow, ms, mspace,
54 | msqrt, mstyle, msub, msubsup, msup, mtable, mtd, mtext, mtr, munder,
55 | munderover, semantics,
56 |
57 | table, caption, colgroup, col, tbody, thead,
58 | tfoot, tr, td, th,
59 |
60 | form, fieldset, legend, label, input, button,
61 | select, datalist, optgroup, option, textarea,
62 | keygen, output, progress, meter,
63 | details, summary, command, menu,
64 |
65 | bdo, dialog, slot, `template`="template"
66 |
67 | const
68 | selfClosing = {area, base, br, col, embed, hr, img, input,
69 | link, meta, param, source, track, wbr}
70 |
71 | svgElements* = {animate .. view}
72 | mathElements* = {maction .. semantics}
73 |
74 | var
75 | svgNamespace* = "http://www.w3.org/2000/svg"
76 | mathNamespace* = "http://www.w3.org/1998/Math/MathML"
77 |
78 | type
79 | EventKind* {.pure.} = enum ## The events supported by the virtual DOM.
80 | onclick, ## An element is clicked.
81 | oncontextmenu, ## An element is right-clicked.
82 | ondblclick, ## An element is double clicked.
83 | onkeyup, ## A key was released.
84 | onkeydown, ## A key is pressed.
85 | onkeypressed, ## A key was pressed.
86 | onfocus, ## An element got the focus.
87 | onblur, ## An element lost the focus.
88 | onchange, ## The selected value of an element was changed.
89 | onscroll, ## The user scrolled within an element.
90 |
91 | onmousedown, ## A pointing device button (usually a mouse) is pressed
92 | ## on an element.
93 | onmouseenter, ## A pointing device is moved onto the element that
94 | ## has the listener attached.
95 | onmouseleave, ## A pointing device is moved off the element that
96 | ## has the listener attached.
97 | onmousemove, ## A pointing device is moved over an element.
98 | onmouseout, ## A pointing device is moved off the element that
99 | ## has the listener attached or off one of its children.
100 | onmouseover, ## A pointing device is moved onto the element that has
101 | ## the listener attached or onto one of its children.
102 | onmouseup, ## A pointing device button is released over an element.
103 |
104 | ondrag, ## An element or text selection is being dragged (every 350ms).
105 | ondragend, ## A drag operation is being ended (by releasing a mouse button
106 | ## or hitting the escape key).
107 | ondragenter, ## A dragged element or text selection enters a valid drop target.
108 | ondragleave, ## A dragged element or text selection leaves a valid drop target.
109 | ondragover, ## An element or text selection is being dragged over a valid
110 | ## drop target (every 350ms).
111 | ondragstart, ## The user starts dragging an element or text selection.
112 | ondrop, ## An element is dropped on a valid drop target.
113 |
114 | onsubmit, ## A form is submitted
115 | oninput, ## An input value changes
116 |
117 | onanimationstart,
118 | onanimationend,
119 | onanimationiteration,
120 |
121 | onkeyupenter, ## vdom extension: an input field received the ENTER key press
122 | onkeyuplater, ## vdom extension: a key was pressed and some time
123 | ## passed (useful for on-the-fly text completions)
124 | onload, ## img
125 |
126 | ontransitioncancel,
127 | ontransitionend,
128 | ontransitionrun,
129 | ontransitionstart,
130 |
131 | onpaste,
132 |
133 | onwheel, ## fires when the user rotates a wheel button on a pointing device.
134 |
135 | onaudioprocess, ## The input buffer of a ScriptProcessorNode is ready to be
136 | ## processed.
137 | oncanplay, ## The browser can play the media, but estimates that not
138 | ## enough data has been loaded to play the media up to its
139 | ## end without having to stop for further buffering of
140 | ## content.
141 | oncanplaythrough, ## The browser estimates it can play the media up to its end
142 | ## without stopping for content buffering.
143 | oncomplete, ## The rendering of an OfflineAudioContext is terminated.
144 | ondurationchange, ## The duration attribute has been updated.
145 | onemptied, ## The media has become empty; for example, this event is
146 | ## sent if the media has already been loaded (or partially
147 | ## loaded), and the load() method is called to reload it.
148 | onended, ## Playback has stopped because the end of the media was
149 | ## reached.
150 | onerror, ## An error occurred while fetching the media data, or the
151 | ## type of the resource is not a supported media format.
152 | onloadeddata, ## The first frame of the media has finished loading.
153 | onloadedmetadata, ## The metadata has been loaded.
154 | onloadstart, ## Fired when the browser has started to load the resource.
155 | onpause, ## Playback has been paused.
156 | onplay, ## Playback has begun.
157 | onplaying, ## Playback is ready to start after having been paused or
158 | ## delayed due to lack of data.
159 | onprogress, ## Fired periodically as the browser loads a resource.
160 | onratechange, ## The playback rate has changed.
161 | onseeked, ## A seek operation completed.
162 | onseeking, ## A seek operation began.
163 | onstalled, ## The user agent is trying to fetch media data, but data is
164 | ## unexpectedly not forthcoming.
165 | onsuspend, ## Media data loading has been suspended.
166 | ontimeupdate, ## The time indicated by the currentTime attribute has been
167 | ## updated.
168 | onvolumechange, ## The volume has changed.
169 | onwaiting, ## Playback has stopped because of a temporary lack of data.
170 |
171 | const
172 | toTag* = block:
173 | var res: array[VNodeKind, kstring]
174 | for kind in VNodeKind:
175 | res[kind] = kstring($kind)
176 | res
177 |
178 | toEventName* = block:
179 | var res: array[EventKind, kstring]
180 | for kind in EventKind:
181 | res[kind] = kstring(($kind)[2..^1])
182 | res
183 |
184 | type
185 | EventHandler* = proc (ev: Event; target: VNode) {.closure.}
186 | NativeEventHandler* = proc (ev: Event) {.closure.}
187 |
188 | EventHandlers* = seq[(EventKind, EventHandler, NativeEventHandler)]
189 |
190 | VKey* = kstring
191 |
192 | VNode* = ref object of RootObj
193 | kind*: VNodeKind
194 | index*: int ## a generally useful 'index'
195 | id*, class*, text*: kstring
196 | kids: seq[VNode]
197 | # even index: key, odd index: value; done this way for memory efficiency:
198 | attrs: seq[kstring]
199 | events*: EventHandlers
200 | when false:
201 | hash*: Hash
202 | validHash*: bool
203 | style*: VStyle ## the style that should be applied to the virtual node.
204 | styleVersion*: int
205 | dom*: Node ## the attached real DOM node. Can be 'nil' if the virtual node
206 | ## is not part of the virtual DOM anymore.
207 |
208 | VComponent* = ref object of VNode ## The abstract class for every karax component.
209 | key*: VKey ## key that determines if two components are
210 | ## identical.
211 | renderImpl*: proc(self: VComponent): VNode
212 | changedImpl*: proc(self, newInstance: VComponent): bool
213 | updatedImpl*: proc(self, newInstance: VComponent)
214 | onAttachImpl*: proc(self: VComponent)
215 | onDetachImpl*: proc(self: VComponent)
216 | version*: int ## Update this to trigger a redraw by karax. Usually you
217 | ## should call 'markDirty' instead which is an alias for
218 | ## 'inc version'.
219 | renderedVersion*: int ## Do not touch. Used by karax. The last version of the
220 | ## component we rendered.
221 | expanded*: VNode ## Do not touch. Used by karax. The VDOM the component
222 | ## expanded to.
223 | debugId*: int
224 |
225 | proc value*(n: VNode): kstring = n.text
226 | proc `value=`*(n: VNode; v: kstring) = n.text = v
227 |
228 | proc intValue*(n: VNode): int = n.index
229 | proc vn*(i: int): VNode = VNode(kind: VNodeKind.int, index: i)
230 | proc vn*(b: bool): VNode = VNode(kind: VNodeKind.int, index: ord(b))
231 | proc vn*(x: kstring): VNode = VNode(kind: VNodeKind.text, index: -1, text: x)
232 |
233 | template callThunk*(fn: typed; n: VNode): untyped =
234 | ## for internal usage only.
235 | fn(n.kids)
236 |
237 | proc vthunk*(name: kstring; args: varargs[VNode, vn]): VNode =
238 | VNode(kind: VNodeKind.vthunk, text: name, index: -1, kids: @args)
239 |
240 | proc dthunk*(dom: Node): VNode =
241 | VNode(kind: VNodeKind.dthunk, dom: dom)
242 |
243 | proc setEventIfNoConflict(v: VNode; kind: EventKind; handler: EventHandler) =
244 | assert handler != nil
245 | for i in 0.. 0:
395 | result.add " " & astToStr(field) & " = " & $n.field
396 |
397 | proc toString*(n: VNode; result: var string; indent: int) =
398 | for i in 1..indent: result.add ' '
399 | if result.len > 0: result.add '\L'
400 | result.add "<" & $n.kind
401 | toStringAttr(id)
402 | toStringAttr(class)
403 | for k, v in attrs(n):
404 | result.add " " & $k & " = " & $v
405 | result.add ">\L"
406 | if n.kind == VNodeKind.text:
407 | result.add n.text
408 | else:
409 | if n.text.len > 0:
410 | result.add " value = "
411 | result.add n.text
412 | for child in items(n):
413 | toString(child, result, indent+2)
414 | for i in 1..indent: result.add ' '
415 | result.add "\L" & $n.kind & ">"
416 |
417 | when false:
418 | proc calcHash*(n: VNode) =
419 | if n.validHash: return
420 | n.validHash = true
421 | var h: Hash = ord n.kind
422 | if n.id != nil:
423 | h &= "id"
424 | h &= n.id
425 | if n.class != nil:
426 | h &= "class"
427 | h &= n.class
428 | if n.key >= 0:
429 | h &= "k"
430 | h &= n.key
431 | for k, v in attrs(n):
432 | h &= " "
433 | h &= k
434 | h &= "="
435 | h &= v
436 | if n.kind == VNodeKind.text or n.text != nil:
437 | h &= "t"
438 | h &= n.text
439 | else:
440 | for child in items(n):
441 | calcHash(child)
442 | h &= child.hash
443 | n.hash = h
444 |
445 |
446 | proc add*(result: var string, n: VNode, indent = 0, indWidth = 2) =
447 | ## adds the textual representation of `n` to `result`.
448 |
449 | proc addEscapedAttr(result: var string, s: kstring) =
450 | # `addEscaped` alternative with less escaped characters.
451 | # Only to be used for escaping attribute values enclosed in double quotes!
452 | for c in items(s):
453 | case c
454 | of '<': result.add("<")
455 | of '>': result.add(">")
456 | of '&': result.add("&")
457 | of '"': result.add(""")
458 | else: result.add(c)
459 |
460 | proc addEscaped(result: var string, s: kstring) =
461 | ## same as ``result.add(escape(s))``, but more efficient.
462 | for c in items(s):
463 | case c
464 | of '<': result.add("<")
465 | of '>': result.add(">")
466 | of '&': result.add("&")
467 | of '"': result.add(""")
468 | of '\'': result.add("'")
469 | of '/': result.add("/")
470 | else: result.add(c)
471 |
472 | proc addIndent(result: var string, indent: int) =
473 | result.add("\n")
474 | for i in 1..indent: result.add(' ')
475 |
476 | if n.kind == VNodeKind.text:
477 | result.addEscaped(n.text)
478 | elif n.kind == VNodeKind.verbatim:
479 | result.add(n.text)
480 | else:
481 | let kind = $n.kind
482 | result.add('<')
483 | result.add(kind)
484 | if n.id.len > 0:
485 | result.add " id=\""
486 | result.addEscapedAttr(n.id)
487 | result.add('"')
488 | if n.class.len > 0:
489 | result.add " class=\""
490 | result.addEscapedAttr(n.class)
491 | result.add('"')
492 | for k, v in attrs(n):
493 | result.add(' ')
494 | result.add(k)
495 | result.add("=\"")
496 | result.addEscapedAttr(v)
497 | result.add('"')
498 | if n.style != nil:
499 | result.add " style=\""
500 | for k, v in pairs(n.style):
501 | if v.len == 0: continue
502 | for t in tokenize($k, seps={'A' .. 'Z'}):
503 | if t.isSep: result.add '-'
504 | result.add toLowerAscii(t.token)
505 | result.add ": "
506 | result.add v
507 | result.add "; "
508 | result.add('"')
509 | if n.len > 0:
510 | result.add('>')
511 | if n.len > 1:
512 | var noWhitespace = false
513 | for i in 0..b`` is
521 | # different from ``a b``.
522 | for i in 0..")
533 | elif n.kind in selfClosing:
534 | result.add(" />")
535 | else:
536 | result.add(">")
537 | result.add("")
538 | result.add(kind)
539 | result.add(">")
540 |
541 |
542 | proc `$`*(n: VNode): kstring =
543 | when defined(js):
544 | var res = ""
545 | toString(n, res, 0)
546 | result = kstring(res)
547 | else:
548 | result = ""
549 | add(result, n)
550 |
551 | proc getVNodeById*(n: VNode; id: cstring): VNode =
552 | ## Get the VNode that was marked with ``id``. Returns ``nil``
553 | ## if no node exists.
554 | if n.id == id: return n
555 | for i in 0.. a:
262 | s.add ""
263 | s.add ""
264 | # insertion point here, shift all remaining pairs by 2 indexes
265 | for j in countdown(s.len-1, i+3, 2):
266 | s[j] = s[j-2]
267 | s[j-1] = s[j-3]
268 | s[i] = a
269 | s[i+1] = value
270 | return
271 | inc i, 2
272 | s.add a
273 | s.add value
274 |
275 | proc setAttr*(s: VStyle; attr: StyleAttr, value: kstring) {.noSideEffect.} =
276 | when kstring is cstring:
277 | assert value != nil, "value must not be nil"
278 | setAttr(s, toStyleAttrName[attr], value)
279 |
280 | proc getAttr*(s: VStyle; attr: StyleAttr): kstring {.noSideEffect.} =
281 | ## returns "" if the attribute has not been set.
282 | var i = 0
283 | let a = toStyleAttrName[attr]
284 | while i < s.len:
285 | if s[i] == a:
286 | return s[i+1]
287 | elif s[i] > a:
288 | return ""
289 | inc i, 2
290 |
291 | proc style*(pairs: varargs[(StyleAttr, kstring)]): VStyle {.noSideEffect.} =
292 | ## constructs a VStyle object from a list of (attribute, value)-pairs.
293 | when defined(js):
294 | result = newJSeq[cstring]()
295 | else:
296 | new(result)
297 | result[] = @[]
298 | for x in pairs:
299 | result.setAttr x[0], x[1]
300 |
301 | proc style*(a: StyleAttr; val: kstring): VStyle {.noSideEffect.} =
302 | ## constructs a VStyle object from a single (attribute, value)-pair.
303 | when defined(js):
304 | result = newJSeq[cstring]()
305 | else:
306 | new(result)
307 | result[] = @[]
308 | result.setAttr a, val
309 |
310 | proc toCss*(a: string): VStyle =
311 | ##[
312 | See example in hellostyle.nim
313 | Allows passing a css string directly, eg:
314 | tdiv(style = style((fontStyle, "italic".kstring), (color, "orange".kstring))): discard
315 | tdiv(style = "font-style: oblique; color: pink".toCss): discard
316 | ]##
317 | when defined(js):
318 | result = newJSeq[cstring]()
319 | else:
320 | new(result)
321 | result[] = @[]
322 | for ai in a.split(";"):
323 | var ai = ai.strip
324 | if ai.len == 0: continue
325 | let aj = ai.strip.split(":", maxsplit=1)
326 | when not defined(release):
327 | if aj.len != 2:
328 | raise newException(ValueError, "Incorrect css rule: " & ai)
329 | result.setAttr(aj[0], aj[1])
330 |
331 | when defined(js):
332 | proc setStyle(d: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.}
333 |
334 | proc applyStyle*(n: Node; s: VStyle) {.noSideEffect.} =
335 | ## apply the style to the real DOM node ``n``.
336 |
337 | #n.style = Style() # optimized, this is a hotspot:
338 | {.emit: "`n`.style = {};".}
339 | for i in countup(0, s.len-1, 2):
340 | n.style.setStyle(s[i], s[i+1])
341 |
342 | proc merge*(a, b: VStyle): VStyle {.noSideEffect.} =
343 | ## merges two styles. ``b`` takes precedence over ``a``.
344 | when defined(js):
345 | result = newJSeq[cstring]()
346 | else:
347 | new(result)
348 | result[] = @[]
349 | for i in 0.. 0:
16 | for k in el:
17 | kids.add k.toXmlNode
18 | newXmlTree($el.kind, kids, attributes = xAttrs)
19 |
20 | proc toVNode*(el: XmlNode): VNode =
21 | try:
22 | case el.kind
23 | of xnElement:
24 | let kind = parseEnum[VNodeKind](el.tag)
25 | var vnAttrs: seq[(string, string)]
26 | if not el.attrs.isnil:
27 | for k, v in el.attrs:
28 | vnAttrs.add (k, v)
29 | var kids: seq[VNode]
30 | if el.len > 0:
31 | for k in el:
32 | kids.add k.toVNode
33 | tree(kind, vnAttrs, kids)
34 | of xnText:
35 | vn($el)
36 | else:
37 | verbatim($el)
38 | except ValueError: # karax doesn't support the node tag
39 | verbatim($el)
40 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 |     
4 |
5 | # Karax
6 | Karax is a framework for developing single page applications in Nim.
7 |
8 | ## Install
9 |
10 | To use Karax you must have nim installed. You can follow the instructions [here](https://nim-lang.org/install.html).
11 |
12 | Then you can install karax through nimble:
13 | ``nimble install karax``
14 |
15 | ## Try Karax
16 | To try it out, run:
17 |
18 | ``cd ~/projects # Insert your favourite directory for projects``
19 |
20 | ``nimble develop karax # This will clone Karax and create a link to it in ~/.nimble``
21 |
22 | ``cd karax``
23 |
24 | ``cd examples/todoapp``
25 |
26 | ``nim js todoapp.nim``
27 |
28 | ``open todoapp.html``
29 |
30 | ``cd ../..``
31 |
32 | ``cd examples/mediaplayer``
33 |
34 | ``nim js playerapp.nim``
35 |
36 | ``open playerapp.html``
37 |
38 | It uses a virtual DOM like React, but is much smaller than the existing
39 | frameworks plus of course it's written in Nim for Nim. No external
40 | dependencies! And thanks to Nim's whole program optimization only what
41 | is used ends up in the generated JavaScript code.
42 |
43 |
44 | ## Goals
45 |
46 |
47 | - Leverage Nim's macro system to produce a framework that allows
48 | for the development of applications that are boilerplate free.
49 | - Keep it small, keep it fast, keep it flexible.
50 |
51 |
52 |
53 | ## Hello World
54 |
55 |
56 | The simplest Karax program looks like this:
57 |
58 | ```nim
59 |
60 | include karax / prelude
61 |
62 | proc createDom(): VNode =
63 | result = buildHtml(tdiv):
64 | text "Hello World!"
65 |
66 | setRenderer createDom
67 | ```
68 |
69 | Since ``div`` is a keyword in Nim, karax choose to use ``tdiv`` instead
70 | here. ``tdiv`` produces a ``
`` virtual DOM node.
71 |
72 | As you can see, karax comes with its own ``buildHtml`` DSL for convenient
73 | construction of (virtual) DOM trees (of type ``VNode``). Karax provides
74 | a tiny build tool called ``karun`` that generates the HTML boilerplate code that
75 | embeds and invokes the generated JavaScript code:
76 |
77 | ```shell
78 | nim c karax/tools/karun
79 | karax/tools/karun -r helloworld.nim
80 | ```
81 |
82 | Via ``-d:debugKaraxDsl`` we can have a look at the produced Nim code by
83 | ``buildHtml``:
84 |
85 | ```nim
86 |
87 | let tmp1 = tree(VNodeKind.tdiv)
88 | add(tmp1, text "Hello World!")
89 | tmp1
90 | ```
91 | (I shortened the IDs for better readability.)
92 |
93 | Ok, so ``buildHtml`` introduces temporaries and calls ``add`` for the tree
94 | construction so that it composes with all of Nim's control flow constructs:
95 |
96 |
97 | ```nim
98 |
99 | include karax / prelude
100 | import random
101 |
102 | proc createDom(): VNode =
103 | result = buildHtml(tdiv):
104 | if rand(100) <= 50:
105 | text "Hello World!"
106 | else:
107 | text "Hello Universe"
108 |
109 | randomize()
110 | setRenderer createDom
111 |
112 | ```
113 | Produces:
114 |
115 | ```nim
116 |
117 | let tmp1 = tree(VNodeKind.tdiv)
118 | if rand(100) <= 50:
119 | add(tmp1, text "Hello World!")
120 | else:
121 | add(tmp1, text "Hello Universe")
122 | tmp1
123 | ```
124 |
125 | ## Event model
126 |
127 | Karax does not change the DOM's event model much, here is a program
128 | that writes "Hello simulated universe" on a button click:
129 |
130 | ```nim
131 |
132 | include karax / prelude
133 | # alternatively: import karax / [kbase, vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, jjson]
134 |
135 | var lines: seq[kstring] = @[]
136 |
137 | proc createDom(): VNode =
138 | result = buildHtml(tdiv):
139 | button:
140 | text "Say hello!"
141 | proc onclick(ev: Event; n: VNode) =
142 | lines.add "Hello simulated universe"
143 | for x in lines:
144 | tdiv:
145 | text x
146 |
147 | setRenderer createDom
148 | ```
149 |
150 | ``kstring`` is Karax's alias for ``cstring`` (which stands for "compatible
151 | string"; for the JS target that is an immutable JavaScript string) which
152 | is preferred for efficiency on the JS target. However, on the native targets
153 | ``kstring`` is mapped to ``string`` for efficiency. The DSL for HTML
154 | construction is also available for the native targets (!) and the ``kstring``
155 | abstraction helps to deal with these conflicting requirements.
156 |
157 | Karax's DSL is quite flexible when it comes to event handlers, so the
158 | following syntax is also supported:
159 |
160 | ```nim
161 |
162 | include karax / prelude
163 | from sugar import `=>`
164 |
165 | var lines: seq[kstring] = @[]
166 |
167 | proc createDom(): VNode =
168 | result = buildHtml(tdiv):
169 | button(onclick = () => lines.add "Hello simulated universe"):
170 | text "Say hello!"
171 | for x in lines:
172 | tdiv:
173 | text x
174 |
175 | setRenderer createDom
176 | ```
177 |
178 | The ``buildHtml`` macro produces this code for us:
179 |
180 | ```nim
181 |
182 | let tmp2 = tree(VNodeKind.tdiv)
183 | let tmp3 = tree(VNodeKind.button)
184 | addEventHandler(tmp3, EventKind.onclick,
185 | () => lines.add "Hello simulated universe", kxi)
186 | add(tmp3, text "Say hello!")
187 | add(tmp2, tmp3)
188 | for x in lines:
189 | let tmp4 = tree(VNodeKind.tdiv)
190 | add(tmp4, text x)
191 | add(tmp2, tmp4)
192 | tmp2
193 | ```
194 | As the examples grow larger it becomes more and more visible of what
195 | a DSL that composes with the builtin Nim control flow constructs buys us.
196 | Once you have tasted this power there is no going back and languages
197 | without AST based macro system simply don't cut it anymore.
198 |
199 |
200 | ## Reactivity
201 |
202 | Karax's reactivity model is different to mainstream frameworks, who usually implement it by creating reactive state. Karax instead reacts to events.
203 |
204 | This approach is simpler and easier to reason about, with the tradeoff being that events need to be wrapped to trigger a redraw. Karax does this for you with dom event handlers (`onclick`, `keyup`, etc) and ajax network calls (when using `karax/kajax`), but you will need to add it for things outside of that (websocket messages, document timing functions, etc).
205 |
206 | `karax/kdom` includes a definition for `setInterval`, the browser api that repeatedly calls a given function. By default it is not reactive, so this is how we might add reactivity with a call to `redraw`:
207 |
208 | ```nim
209 | include karax/prelude
210 | import karax/kdom except setInterval
211 |
212 | proc setInterval(cb: proc(), interval: int): Interval {.discardable.} =
213 | kdom.setInterval(proc =
214 | cb()
215 | if not kxi.surpressRedraws: redraw(kxi)
216 | , interval)
217 |
218 | var v = 10
219 |
220 | proc update =
221 | v += 10
222 |
223 | setInterval(update, 200)
224 |
225 | proc main: VNode =
226 | buildHtml(tdiv):
227 | text $v
228 |
229 | setRenderer main
230 | ```
231 |
232 |
233 | ## Attaching data to an event handler
234 |
235 |
236 | Since the type of an event handler is ``(ev: Event; n: VNode)`` or ``()`` any
237 | additional data that should be passed to the event handler needs to be
238 | done via Nim's closures. In general this means a pattern like this:
239 |
240 | ```nim
241 |
242 | proc menuAction(menuEntry: kstring): proc() =
243 | result = proc() =
244 | echo "clicked ", menuEntry
245 |
246 | proc buildMenu(menu: seq[kstring]): VNode =
247 | result = buildHtml(tdiv):
248 | for m in menu:
249 | nav(class="navbar is-primary"):
250 | tdiv(class="navbar-brand"):
251 | a(class="navbar-item", onclick = menuAction(m)):
252 | ```
253 |
254 | ## DOM diffing
255 |
256 | Ok, so now we have seen DOM creation and event handlers. But how does
257 | Karax actually keep the DOM up to date? The trick is that every event
258 | handler is wrapped in a helper proc that triggers a *redraw* operation
259 | that calls the *renderer* that you initially passed to ``setRenderer``.
260 | So a new virtual DOM is created and compared against the previous
261 | virtual DOM. This comparison produces a patch set that is then applied
262 | to the real DOM the browser uses internally. This process is called
263 | "virtual DOM diffing" and other frameworks, most notably Facebook's
264 | *React*, do quite similar things. The virtual DOM is faster to create
265 | and manipulate than the real DOM so this approach is quite efficient.
266 |
267 |
268 | ## Form validation
269 | Most applications these days have some "login"
270 | mechanism consisting of ``username`` and ``password`` and
271 | a ``login`` button. The login button should only be clickable
272 | if ``username`` and ``password`` are not empty. An error
273 | message should be shown as long as one input field is empty.
274 |
275 | To create new UI elements we write a ``loginField`` proc that
276 | returns a ``VNode``:
277 |
278 | ```nim
279 |
280 | proc loginField(desc, field, class: kstring;
281 | validator: proc (field: kstring): proc ()): VNode =
282 | result = buildHtml(tdiv):
283 | label(`for` = field):
284 | text desc
285 | input(class = class, id = field, onchange = validator(field))
286 | ```
287 |
288 | We use the ``karax / errors`` module to help with this error
289 | logic. The ``errors`` module is mostly a mapping from strings to
290 | strings but it turned out that the logic is tricky enough to warrant
291 | a library solution. ``validateNotEmpty`` returns a closure that
292 | captures the ``field`` parameter:
293 |
294 | ```nim
295 |
296 | proc validateNotEmpty(field: kstring): proc () =
297 | result = proc () =
298 | let x = getVNodeById(field).getInputText
299 | if x.isNil or x == "":
300 | errors.setError(field, field & " must not be empty")
301 | else:
302 | errors.setError(field, "")
303 | ```
304 |
305 | This indirection is required because
306 | event handlers in Karax need to have the type ``proc ()``
307 | or ``proc (ev: Event; n: VNode)``. The errors module also
308 | gives us a handy ``disableOnError`` helper. It returns
309 | ``"disabled"`` if there are errors. Now we have all the
310 | pieces together to write our login dialog:
311 |
312 |
313 | ```nim
314 |
315 | # some consts in order to prevent typos:
316 | const
317 | username = kstring"username"
318 | password = kstring"password"
319 |
320 | var loggedIn: bool
321 |
322 | proc loginDialog(): VNode =
323 | result = buildHtml(tdiv):
324 | if not loggedIn:
325 | loginField("Name :", username, "input", validateNotEmpty)
326 | loginField("Password: ", password, "password", validateNotEmpty)
327 | button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()):
328 | text "Login"
329 | p:
330 | text errors.getError(username)
331 | p:
332 | text errors.getError(password)
333 | else:
334 | p:
335 | text "You are now logged in."
336 |
337 | setRenderer loginDialog
338 | ```
339 |
340 | (Full example [here](https://github.com/karaxnim/karax/blob/master/examples/login.nim).)
341 |
342 | This code still has a bug though, when you run it, the ``login`` button is not
343 | disabled until some input fields are validated! This is easily fixed,
344 | at initialization we have to do:
345 |
346 | ```nim
347 |
348 | setError username, username & " must not be empty"
349 | setError password, password & " must not be empty"
350 | ```
351 | There are likely more elegant solutions to this problem.
352 |
353 | ## Boolean attributes
354 |
355 | Some HTML attributes don't have meaningful values; instead, they are treated like
356 | a boolean whose value is `false` when the attribute is not set, and `true` when
357 | the attribute is set to any value. Some examples of these attributes are `disabled`
358 | and `contenteditable`.
359 |
360 | In Karax, these attributes can be set/cleared with a boolean value:
361 |
362 | ```nim
363 | proc submitButton(dataIsValid: bool): VNode =
364 | buildHtml(tdiv):
365 | button(disabled = not dataIsValid):
366 | if dataIsValid:
367 | text "Submit"
368 | else:
369 | text "Cannot submit, data is invalid!"
370 | ```
371 |
372 | ## Routing
373 |
374 |
375 | For routing ``setRenderer`` can be called with a callback that takes a parameter of
376 | type ``RouterData``. Here is the relevant excerpt from the famous "Todo App" example:
377 |
378 | ```nim
379 |
380 | proc createDom(data: RouterData): VNode =
381 | if data.hashPart == "#/": filter = all
382 | elif data.hashPart == "#/completed": filter = completed
383 | elif data.hashPart == "#/active": filter = active
384 | result = buildHtml(tdiv(class="todomvc-wrapper")):
385 | section(class = "todoapp"):
386 | ...
387 |
388 | setRenderer createDom
389 | ```
390 | (Full example [here](https://github.com/karaxnim/karax/blob/master/examples/todoapp/todoapp.nim).)
391 |
392 | ## Server Side HTML Rendering
393 |
394 | Karax can also be used to render HTML on the server. Only a subset of
395 | modules can be used since there is no JS interpreter.
396 |
397 | ```nim
398 |
399 | import karax / [karaxdsl, vdom]
400 |
401 | const places = @["boston", "cleveland", "los angeles", "new orleans"]
402 |
403 | proc render*(): string =
404 | let vnode = buildHtml(tdiv(class = "mt-3")):
405 | h1: text "My Web Page"
406 | p: text "Hello world"
407 | ul:
408 | for place in places:
409 | li: text place
410 | dl:
411 | dt: text "Can I use Karax for client side single page apps?"
412 | dd: text "Yes"
413 |
414 | dt: text "Can I use Karax for server side HTML rendering?"
415 | dd: text "Yes"
416 | result = $vnode
417 |
418 | echo render()
419 | ```
420 |
421 | You can embed raw html using the `verbatim` proc:
422 |
423 | ``` nim
424 | let vg = """
425 |
429 | """
430 | let wrap = buildHtml(tdiv(class="wrapper")):
431 | verbatim(vg)
432 |
433 | echo wrap
434 | ```
435 |
436 | ## Generate HTML with event handlers
437 |
438 | If you are writing a static site generator or do server-side HTML rendering
439 | via ``nim c``, you may want to override ``addEventHandler`` when using event
440 | handlers to avoid compiler complaints.
441 |
442 | Here's an example of auto submit a dropdown when a value is selected:
443 |
444 | ```nim
445 |
446 | template kxi(): int = 0
447 | template addEventHandler(n: VNode; k: EventKind; action: string; kxi: int) =
448 | n.setAttr($k, action)
449 |
450 | let
451 | names = @["nim", "c", "python"]
452 | selected_name = request.params.getOrDefault("name")
453 | hello = buildHtml(html):
454 | form(`method` = "get"):
455 | select(name="name", onchange="this.form.submit()"):
456 | for name in names:
457 | if name == selected_name:
458 | option(selected = ""): text name
459 | else:
460 | option: text name
461 | ```
462 |
463 | ## Debugging
464 |
465 | Karax will accept various compile time flags to add additional checks and debug info.
466 |
467 | e.g. `nim js -d:debugKaraxDsl myapp.nim`
468 |
469 | | flag name | description |
470 | | --------------- | ----------- |
471 | | debugKaraxDsl | prints the Nim code produced by the `buildHtml` macro to the terminal at compile time |
472 | | debugKaraxSame | Ensures that the rendered html dom matches the expected output from the vdom. Note that some browser extensions will modify the page and cause false positives |
473 | | karaxDebug* | prints debug info when checking the dom output and applying component state |
474 | | stats* | track statistics about recursion depth when rendering |
475 | | profileKarax* | track statistics about why nodes differ |
476 |
477 | _* = used when debugging karax itself, not karax apps_
478 |
479 | ## License
480 | MIT License. See [here](https://github.com/karaxnim/karax/blob/master/LICENSE.txt).
481 |
--------------------------------------------------------------------------------
/tests/blur.nim:
--------------------------------------------------------------------------------
1 | import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils
2 |
3 | type TextInput* = ref object of VComponent
4 | value, guid: cstring
5 | isActive: bool
6 | onchange: proc (value: cstring)
7 |
8 | proc render(x: VComponent): VNode =
9 | let self = TextInput(x)
10 |
11 | let style = style(
12 | (StyleAttr.position, cstring"relative"),
13 | (StyleAttr.paddingLeft, cstring"10px"),
14 | (StyleAttr.paddingRight, cstring"5px"),
15 | (StyleAttr.height, cstring"30px"),
16 | (StyleAttr.lineHeight, cstring"30px"),
17 | (StyleAttr.border, cstring"solid 1px " & (if self.isActive: cstring"red" else: cstring"black")),
18 | (StyleAttr.fontSize, cstring"12px"),
19 | (StyleAttr.fontWeight, cstring"600")
20 | ).merge(self.style)
21 |
22 | let inputStyle = style.merge(style(
23 | (StyleAttr.color, cstring"inherit"),
24 | (StyleAttr.fontSize, cstring"inherit"),
25 | (StyleAttr.fontWeight, cstring"inherit"),
26 | (StyleAttr.fontFamily, cstring"inherit"),
27 | (StyleAttr.position, cstring"absolute"),
28 | (StyleAttr.top, cstring"0"),
29 | (StyleAttr.left, cstring"0"),
30 | (StyleAttr.height, cstring"100%"),
31 | (StyleAttr.width, cstring"100%"),
32 | (StyleAttr.border, cstring"none"),
33 | (StyleAttr.backgroundColor, cstring"transparent"),
34 | ))
35 |
36 | proc flip(ev: Event; n: VNode) =
37 | self.isActive = not self.isActive
38 | echo "flip! ", self.isActive, " id: ", self.debugId, " version ", self.version
39 | markDirty(self)
40 |
41 | proc onchanged(ev: Event; n: VNode) =
42 | if self.onchange != nil:
43 | self.onchange n.value
44 | self.value = n.value
45 |
46 | result = buildHtml(tdiv(style=style)):
47 | input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip, onkeyup=onchanged)
48 |
49 | proc changed(current, next: VComponent): bool =
50 | let current = TextInput(current)
51 | let next = TextInput(next)
52 | if current.guid != next.guid:
53 | result = true
54 | else:
55 | result = defaultChangedImpl(current, next)
56 |
57 | proc update(current, next: VComponent) =
58 | let current = TextInput(current)
59 | let next = TextInput(next)
60 | current.value = next.value
61 | current.guid = next.guid
62 | #next.isActive = current.isActive
63 |
64 | proc newTextInput*(style: VStyle = VStyle(); guid: cstring; value: cstring = cstring"",
65 | onchange: proc(v: cstring) = nil): TextInput =
66 | result = newComponent(TextInput, render, changed=changed, updated=update)
67 | result.style = style
68 | result.value = value
69 | result.onchange = onchange
70 | result.guid = guid
71 |
72 | when false:
73 | type
74 | Combined = ref object of VComponent
75 | a, b: TextInput
76 |
77 | proc renderComb(self: VComponent): VNode =
78 | let self = Combined(self)
79 |
80 | proc bu(ev: Event; n: VNode) =
81 | self.a.value = ""
82 | self.b.value = ""
83 | markDirty(self.a)
84 | markDirty(self.b)
85 |
86 | result = buildHtml(tdiv(style=self.style)):
87 | self.a
88 | self.b
89 | button(onclick=bu):
90 | text "reset"
91 |
92 | proc changed(self: VComponent): bool =
93 | let self = Combined(self)
94 | result = self.a.changedImpl(self.a) or self.b.changedImpl(self.b)
95 |
96 | proc newCombined*(style: VStyle = VStyle()): Combined =
97 | result = newComponent(Combined, renderComb, changed=changed)
98 | result.a = newTextInput(style, "AAA")
99 | result.b = newTextInput(style, "BBB")
100 |
101 |
102 | var
103 | persons: seq[cstring] = @[cstring"Karax", "Abathur", "Fenix"]
104 | selected = -1
105 | errmsg = cstring""
106 |
107 | proc renderPerson(text: cstring, index: int): VNode =
108 | proc select(ev: Event, n: VNode) =
109 | selected = index
110 | errmsg = ""
111 |
112 | result = buildHtml():
113 | tdiv:
114 | text text
115 | button(onClick = select)
116 |
117 | proc createDom(): VNode =
118 | result = buildHtml(tdiv):
119 | tdiv:
120 | for index, text in persons.pairs:
121 | renderPerson(text, index)
122 | tdiv:
123 | newTextInput(VStyle(), &selected, if selected >= 0: persons[selected] else: "", proc (v: cstring) =
124 | if v.len > 0:
125 | if selected >= 0: persons[selected] = v
126 | errmsg = ""
127 | else:
128 | errmsg = "name must not be empty"
129 | )
130 | tdiv:
131 | text errmsg
132 |
133 | setRenderer createDom
134 |
--------------------------------------------------------------------------------
/tests/compiler_tests.nim:
--------------------------------------------------------------------------------
1 | include ../karax/prelude
2 |
3 | block:
4 | type
5 | TestEnum {.pure.} = enum
6 | ValA, ValB
7 |
8 | var e = TestEnum.ValA
9 |
10 | proc renderDom(): VNode {.used.} =
11 | result = buildHtml():
12 | tdiv():
13 | case e
14 | of TestEnum.ValA:
15 | tdiv: text "A"
16 | of TestEnum.ValB:
17 | tdiv: text "B"
18 |
19 |
--------------------------------------------------------------------------------
/tests/components.nim:
--------------------------------------------------------------------------------
1 | import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils
2 |
3 | type TextInput* = ref object of VComponent
4 | value: cstring
5 | isActive: bool
6 |
7 | proc render(x: VComponent): VNode =
8 | let self = TextInput(x)
9 |
10 | let style = style(
11 | (StyleAttr.position, cstring"relative"),
12 | (StyleAttr.paddingLeft, cstring"10px"),
13 | (StyleAttr.paddingRight, cstring"5px"),
14 | (StyleAttr.height, cstring"30px"),
15 | (StyleAttr.lineHeight, cstring"30px"),
16 | (StyleAttr.border, cstring"solid 1px " & (if self.isActive: cstring"red" else: cstring"black")),
17 | (StyleAttr.fontSize, cstring"12px"),
18 | (StyleAttr.fontWeight, cstring"600")
19 | ).merge(self.style)
20 |
21 | let inputStyle = style.merge(style(
22 | (StyleAttr.color, cstring"inherit"),
23 | (StyleAttr.fontSize, cstring"inherit"),
24 | (StyleAttr.fontWeight, cstring"inherit"),
25 | (StyleAttr.fontFamily, cstring"inherit"),
26 | (StyleAttr.position, cstring"absolute"),
27 | (StyleAttr.top, cstring"0"),
28 | (StyleAttr.left, cstring"0"),
29 | (StyleAttr.height, cstring"100%"),
30 | (StyleAttr.width, cstring"100%"),
31 | (StyleAttr.border, cstring"none"),
32 | (StyleAttr.backgroundColor, cstring"transparent"),
33 | ))
34 |
35 | proc flip(ev: Event; n: VNode) =
36 | self.isActive = not self.isActive
37 | echo "flip! ", self.isActive, " id: ", self.debugId, " version ", self.version
38 | markDirty(self)
39 |
40 | result = buildHtml(tdiv(style=style)):
41 | input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip,
42 | events=self.events)
43 |
44 | proc update(current, next: VComponent) =
45 | let current = TextInput(current)
46 | let next = TextInput(next)
47 | current.value = next.value
48 | current.key = next.key
49 |
50 | proc changed(current, next: VComponent): bool = true
51 |
52 | proc newTextInput*(style: VStyle = VStyle(); key: cstring;
53 | value = cstring""): TextInput =
54 | result = newComponent(TextInput, render, changed=changed, updated=update)
55 | result.style = style
56 | result.value = value
57 | #result.key = key
58 |
59 | when false:
60 | type
61 | Combined = ref object of VComponent
62 | a, b: TextInput
63 |
64 | proc renderComb(self: VComponent): VNode =
65 | let self = Combined(self)
66 |
67 | proc bu(ev: Event; n: VNode) =
68 | self.a.value = ""
69 | self.b.value = ""
70 | markDirty(self.a)
71 | markDirty(self.b)
72 |
73 | result = buildHtml(tdiv(style=self.style)):
74 | self.a
75 | self.b
76 | button(onclick=bu):
77 | text "reset"
78 |
79 | proc changed(self: VComponent): bool =
80 | let self = Combined(self)
81 | result = self.a.changedImpl(self.a) or self.b.changedImpl(self.b)
82 |
83 | proc newCombined*(style: VStyle = VStyle()): Combined =
84 | result = newComponent(Combined, renderComb, changed=changed)
85 | result.a = newTextInput(style, "AAA")
86 | result.b = newTextInput(style, "BBB")
87 |
88 |
89 | var
90 | persons: seq[cstring] = @[cstring"Karax", "Abathur", "Fenix"]
91 | errmsg = cstring""
92 |
93 | proc renderPerson(text: cstring, index: int): VNode =
94 | result = buildHtml(tdiv):
95 | newTextInput(VStyle(), &index, text):
96 | proc onkeyuplater(ev: Event; n: VNode) =
97 | let v = n.value
98 | if v.len > 0:
99 | persons[index] = v
100 | errmsg = ""
101 | else:
102 | errmsg = "name must not be empty"
103 | button:
104 | proc onclick(ev: Event; n: VNode) =
105 | persons.delete(index)
106 | errmsg = ""
107 | echo persons
108 | text "(x)"
109 |
110 |
111 | proc createDom(): VNode =
112 | result = buildHtml(tdiv):
113 | tdiv:
114 | for index, text in persons.pairs:
115 | renderPerson(text, index)
116 | tdiv:
117 | newTextInput(VStyle(), &persons.len, ""):
118 | proc onkeyupenter(ev: Event; n: VNode) =
119 | let v = n.value
120 | if v.len > 0:
121 | persons.add v
122 | errmsg = ""
123 | else:
124 | errmsg = "name must not be empty"
125 | tdiv:
126 | text errmsg
127 |
128 | setRenderer createDom
129 |
--------------------------------------------------------------------------------
/tests/diffDomTests.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Diff dom tests
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/tests/diffDomTests.nim:
--------------------------------------------------------------------------------
1 |
2 | import kdom, vdom, times, karax, karaxdsl, jdict, jstrutils, parseutils, sequtils
3 |
4 | var
5 | entries: seq[cstring]
6 | results: seq[cstring]
7 |
8 | proc reset() =
9 | entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
10 | redrawSync()
11 |
12 | proc checkOrder(order: seq[int]): bool =
13 | var ul = getElementById("ul")
14 | if ul == nil or len(ul.children) != len(order):
15 | kout ul, len(order)
16 | return false
17 | var pos = 0
18 | for child in ul.children:
19 | if child.id != $order[pos]:
20 | kout pos
21 | return false
22 | inc pos
23 | return true
24 |
25 | proc check(name: cstring; order: seq[int]) =
26 | let result = checkOrder(order)
27 | results.add name & (if result: cstring" - OK" else: cstring" -FAIL")
28 |
29 | proc test1() =
30 | results.add cstring"test1 started"
31 | entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
32 | entries.insert(cstring("7"), 5)
33 | redrawSync()
34 | check("test1", @[0, 1, 2, 3, 4, 7, 5])
35 |
36 | proc test2() =
37 | results.add cstring"test2 started"
38 | entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
39 | entries.insert(cstring("7"), 5)
40 | entries.insert(cstring("8"), 0)
41 | redrawSync()
42 | check("test2", @[8, 0, 1, 2, 3, 4, 7, 5])
43 |
44 | proc test3() =
45 | results.add cstring"test3 started"
46 | entries = @[cstring("2"), cstring("3"), cstring("4"), cstring("1")]
47 | redrawSync()
48 | check("test3", @[2, 3, 4, 1])
49 |
50 | proc test4() =
51 | results.add cstring"test4 started"
52 | entries = @[cstring("5"), cstring("6"), cstring("7"), cstring("8") ]
53 | redrawSync()
54 | check("test4", @[5, 6, 7, 8])
55 |
56 | proc test5() =
57 | results.add cstring"test5 started"
58 | entries = @[cstring("0"), cstring("1"), cstring("3"), cstring("5"), cstring("4"), cstring("5")]
59 | redrawSync()
60 | check("test 5", @[0, 1, 3, 5, 4, 5])
61 |
62 | proc test6() =
63 | results.add cstring"test6 started"
64 | entries = @[]
65 | redrawSync()
66 | check("test 6", @[])
67 |
68 | # result: 2
69 | proc test7() =
70 | results.add cstring"test7 started"
71 | entries = @[cstring("2")]
72 | redrawSync()
73 | check("test 7", @[2])
74 |
75 | proc createEntry(id: int): VNode =
76 | result = buildHtml():
77 | button(id="" & $id):
78 | text $id
79 |
80 | proc createDom(): VNode =
81 | result = buildHtml(tdiv()):
82 | ul(id="ul"):
83 | for e in entries:
84 | createEntry(parseInt(e))
85 | for r in results:
86 | tdiv:
87 | text r
88 |
89 | proc onload() =
90 | for i in 0..5: # 0_000:
91 | entries.add(cstring($i))
92 | test1()
93 | reset()
94 | test2()
95 | reset()
96 | test3()
97 | reset()
98 | test4()
99 | reset()
100 | test5()
101 | reset()
102 | test6()
103 | reset()
104 | test7()
105 |
106 | setRenderer createDom
107 | onload()
108 |
--------------------------------------------------------------------------------
/tests/difftest.nim:
--------------------------------------------------------------------------------
1 |
2 | include "../karax/karax"
3 | import "../karax/karaxdsl"
4 |
5 | proc hasDom(n: Vnode) =
6 | if n.kind in {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}:
7 | discard
8 | else:
9 | doAssert n.dom != nil
10 | for i in 0..= expected.len:
32 | echo "patches differ; expected nothing but got: ", p
33 | inc err
34 | elif p != expected[j]:
35 | echo "patches differ; expected ", expected[i], " but got: ", p
36 | inc err
37 | inc j
38 | #hasDom(kxi.currentTree)
39 | kxi.patchLen = 0
40 |
41 | proc testAppend() =
42 | let a = buildHtml(tdiv):
43 | ul:
44 | li: text "A"
45 | li: text "B"
46 | let b = buildHtml(tdiv):
47 | ul:
48 | li: text "A"
49 | li: text "B"
50 | li:
51 | tdiv:
52 | text "C"
53 | doDiff(a, b, "pkAppend li div C")
54 |
55 | proc testInsert() =
56 | let a = buildHtml(tdiv):
57 | ul:
58 | li: text "A"
59 | li:
60 | button:
61 | text "C"
62 | let b = buildHtml(tdiv):
63 | ul:
64 | li: text "A"
65 | li: text "B"
66 | li:
67 | button:
68 | text "C"
69 | doDiff(a, b, "pkInsertBefore li B")
70 |
71 | proc testInsert2() =
72 | let a = buildHtml(tdiv):
73 | tdiv:
74 | tdiv:
75 | ul:
76 | li: text "A"
77 | li:
78 | button:
79 | text "D"
80 | let b = buildHtml(tdiv):
81 | tdiv:
82 | tdiv:
83 | ul:
84 | li: text "A"
85 | li: text "B"
86 | li: text "C"
87 | li:
88 | button:
89 | text "D"
90 | doDiff(a, b, "pkInsertBefore li B", "pkInsertBefore li C")
91 |
92 | proc testDelete() =
93 | let a = buildHtml(tdiv):
94 | ul:
95 | li: text "A"
96 | li: text "B"
97 | li: text "C"
98 | let b = buildHtml(tdiv):
99 | ul:
100 | discard
101 | doDiff(a, b, "pkDetach li A", "pkRemove nil",
102 | "pkDetach li B", "pkRemove nil",
103 | "pkDetach li C", "pkRemove nil")
104 |
105 | proc testDeleteMiddle() =
106 | let a = buildHtml(tdiv):
107 | ul:
108 | li:
109 | tdiv: text "A"
110 | li:
111 | tdiv: text "B"
112 | li:
113 | tdiv: text "C"
114 | li:
115 | tdiv: text "D"
116 | li:
117 | tdiv: text "E"
118 | li:
119 | tdiv: text "F"
120 | li:
121 | tdiv: text "G"
122 | li:
123 | button:
124 | tdiv: text "H"
125 | let b = buildHtml(tdiv):
126 | ul:
127 | li:
128 | tdiv: text "A"
129 | li:
130 | tdiv: text "B"
131 | li:
132 | tdiv: text "C"
133 | li:
134 | tdiv: text "D"
135 | li:
136 | tdiv: text "E"
137 | li:
138 | tdiv: text "F"
139 | li:
140 | button:
141 | tdiv: text "H"
142 | doDiff(a, b, "pkDetach li div G", "pkRemove nil")
143 |
144 | proc createEntry(id: cstring): VNode =
145 | result = buildHtml():
146 | button(id="" & id):
147 | text id
148 |
149 | proc createEntries(entries: seq[cstring]): VNode =
150 | result = buildHtml(tdiv()):
151 | ul(id="ul"):
152 | for e in entries:
153 | createEntry(e)
154 | for r in entries:
155 | tdiv:
156 | text r
157 |
158 | proc testWild() =
159 | var entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring"7", cstring("5")]
160 | let a = createEntries(entries)
161 | entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
162 | let b = createEntries(entries)
163 | doDiff(a, b, "pkDetach button 7", "pkRemove nil", "pkDetach div 7", "pkRemove nil")
164 |
165 | proc testWildInsert() =
166 | var entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
167 | let a = createEntries(entries)
168 | entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring"7", cstring("5")]
169 | let b = createEntries(entries)
170 | doDiff(a, b, "pkInsertBefore button 7", "pkInsertBefore div 7")
171 |
172 | kxi = KaraxInstance(rootId: cstring"ROOT", renderer: proc (data: RouterData): VNode = discard,
173 | byId: newJDict[cstring, VNode]())
174 |
175 | testAppend()
176 | testInsert()
177 | testInsert2()
178 | testDelete()
179 | testWild()
180 | testWildInsert()
181 | testDeleteMiddle()
182 | if err == 0:
183 | echo "Success"
184 | else:
185 | quit 1
186 |
--------------------------------------------------------------------------------
/tests/domEventTests.css:
--------------------------------------------------------------------------------
1 | #dragDiv {
2 | background-color: #0d1224;
3 | width: 250px;
4 | height: 100px;
5 | position: absolute;
6 | top: 500px;
7 | left: 200px;
8 | text-align: center;
9 | cursor: move;
10 | color: rgb(60, 201, 163);
11 | }
12 |
13 | #touchDiv {
14 | background-color: #0d1224;
15 | width: 250px;
16 | height: 100px;
17 | position: absolute;
18 | top: 500px;
19 | left: 500px;
20 | text-align: center;
21 | cursor: move;
22 | color: rgb(60, 201, 163);
23 | }
24 |
25 | #results {
26 | font-family: monospace;
27 | font-size: 10pt;
28 | width: 100%;
29 | height: 400px;
30 | overflow: auto;
31 | background-color: #000000;
32 | color: forestgreen;
33 | }
34 |
35 | #test10input1, #test10input2, #test11h3, #test12input {
36 | visibility: hidden;
37 | }
38 |
39 | #test11h3 {
40 | color: gold;
41 | }
42 |
43 | body {
44 | background-color: dimgrey;
45 | }
46 |
--------------------------------------------------------------------------------
/tests/domEventTests.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | DOM Event Tests
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/tests/lists.nim:
--------------------------------------------------------------------------------
1 | import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils
2 |
3 | var contents: seq[cstring] = @[]
4 |
5 | proc onTodoEnter(e: Event; n: VNode) =
6 | contents.insert(n.value & "BBBB")
7 | contents.insert(n.value)
8 | n.value = ""
9 |
10 | proc createDom(): VNode =
11 | result = buildHtml(tdiv):
12 | input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
13 | onkeyupenter = onTodoEnter, setFocus)
14 | for c in contents:
15 | tdiv:
16 | text c
17 |
18 | setRenderer createDom
19 |
--------------------------------------------------------------------------------
/tests/nativehtmlgen.nim:
--------------------------------------------------------------------------------
1 | ## Simple test that shows Karax can also do client-side HTML rendering.
2 |
3 | import "../karax" / [karaxdsl, vdom]
4 |
5 | when defined(js):
6 | {.error: "Use 'nim c' to compile this example".}
7 |
8 | template kxi(): int = 0
9 | template addEventHandler(n: VNode; k: EventKind; action: string; kxi: int) =
10 | n.setAttr($k, action)
11 |
12 | let tab = buildHtml(table):
13 | tr:
14 | td:
15 | text "Cell A"
16 | td:
17 | text "Cell B"
18 | tr:
19 | td:
20 | text "Cell C"
21 | td:
22 | text "Cell D"
23 | td:
24 | a(href = "#/", onclick = "javascript:myFunc()"):
25 | text"haha"
26 |
27 | echo tab
28 |
--------------------------------------------------------------------------------
/tests/nim.cfg:
--------------------------------------------------------------------------------
1 | --path: "../karax"
2 |
--------------------------------------------------------------------------------
/tests/scope.nim:
--------------------------------------------------------------------------------
1 | include karax / prelude
2 |
3 | proc render():VNode =
4 | build_html tdiv:
5 | block:
6 | let a = 1
7 | tdiv():
8 | text $a
9 | block:
10 | let a = 2
11 | tdiv():
12 | text $a
13 |
14 | setRenderer render
15 |
--------------------------------------------------------------------------------
/tests/tester.nim:
--------------------------------------------------------------------------------
1 | # For now we only test that the things still compile.
2 |
3 | import os, osproc, streams
4 | import parseutils
5 |
6 | proc exec(cmd: string) =
7 | if os.execShellCmd(cmd) != 0:
8 | quit "command failed " & cmd
9 |
10 | proc main =
11 | for guide in os.walkDirRec("guide"):
12 | let contents = guide.readFile
13 | var last = 0
14 | var trash = ""
15 | const startDelim = "```nim"
16 | const endDelim = "```"
17 | while true:
18 | last += contents.parseUntil(trash, startDelim, last)
19 | if last == contents.len: break # no matches found
20 | var code = ""
21 | last += contents.parseUntil(code, endDelim, last+startDelim.len)
22 | var snippet = startProcess("nim js -", options = {poStdErrToStdOut, poUsePath, poEvalCommand})
23 | var codeStream = snippet.inputStream
24 | codeStream.write(code & "\0")
25 | codeStream.close()
26 | var res = snippet.waitForExit
27 | echo snippet.outputStream.readAll
28 | if res != 0:
29 | echo code
30 | quit "Failed to compile"
31 | snippet.close()
32 | exec("nim js tests/diffDomTests.nim")
33 | exec("nim js tests/compiler_tests.nim")
34 | exec("nim js examples/todoapp/todoapp.nim")
35 | exec("nim js examples/scrollapp/scrollapp.nim")
36 | exec("nim js examples/mediaplayer/playerapp.nim")
37 | exec("nim js examples/carousel/carousel.nim")
38 | exec("nim js -d:nodejs -r tests/difftest.nim")
39 | exec("nim c tests/nativehtmlgen.nim")
40 | exec("nim c tests/xmlNodeConversionTests.nim")
41 |
42 | for test in os.walkFiles("examples/*.nim"):
43 | exec("nim js " & test)
44 |
45 | main()
46 |
--------------------------------------------------------------------------------
/tests/xmlNodeConversionTests.nim:
--------------------------------------------------------------------------------
1 | import std/xmltree
2 | import "../karax"/[vdom, xdom]
3 |
4 | let xn = newXmlTree("div", @[], attributes = [("a", "b")].toXmlAttributes)
5 | let vn = tree(tdiv, attrs = (@[("a", "b"), ("c", "d")]))
6 | vn.add newVNode(a)
7 | vn[0].add vn("bbb")
8 | vn.add verbatim("abc")
9 | xn.add newXmlTree("i", @[], attributes = [("a", "b")].toXmlAttributes)
10 |
11 | let vnX = vn.toXmlNode
12 |
13 | doassert vnX[0].len == 1 and vnX[1][0].kind == xnText
14 | doassert $vnX[1].tag == "app"
15 | doassert $vnX == """