Compare commits

...

292 commits

Author SHA1 Message Date
geotre
85c81d8c0e
Fix issue #195 (#196) 2021-05-15 21:22:56 +02:00
geotre
6d51d7d08c
Fix issue #192 (#197) 2021-05-15 21:22:33 +02:00
Arham Jain
93d9b73867
Moved CI from Travis to Github Actions (#198)
* Create ci.yml

* removed travis build file

* changed docker image

* Update readme to include CI results
2021-05-06 22:56:55 +02:00
Andreas Rumpf
ac210fb634
Update readme.md 2021-05-06 17:57:48 +02:00
Willyboar
df828e145d
beautify README (#194)
* beautify README

* Fix small issues

* Revert "Fix small issues"

This reverts commit 2a480addd7e2a55faaa95dd2145a6dc0aa4b95f7.

* Small Fixes vol.2

* Fix licence link

Co-authored-by: codewski <devon@codewski.com>
Co-authored-by: ΝΙΚΟΣ <nikos@Macintosh.local>
2021-05-06 17:57:08 +02:00
Arham Jain
cedd39a65f
Remove kout deprecation (#193) 2021-05-03 10:26:15 +02:00
Matt Haggard
12b98b2338
Make 'karun -w -r ...' do watching for more than one iteration (#190) 2021-03-31 11:43:43 +02:00
flywind
8a7f05ac2a
Update readme.rst (#184) 2021-02-09 11:55:46 +01:00
Antonis Geralis
0fca6e961d
add all svg elements acording to the standard (#178) 2021-02-04 19:24:19 +01:00
Double_oxygeN
32de202845
add picture tag as a kind of virtual DOM (#174) 2021-01-25 07:51:24 +01:00
Antonis Geralis
6cff2d8b05
Refactor static_server (#169) 2021-01-08 17:07:19 +01:00
Araq
4c06cb3204 1.2.1 2020-12-08 18:03:14 +01:00
Araq
9a627985d0 added a getInputText API, cleaner and more robust than the .text attribute which is often updated too late in event handlers after a redraw 2020-12-08 18:02:59 +01:00
Constantine Molchanov
24a037ad19
Get rid of annoying deprecation warnings (#164)
* Get rid of annoying deprecation warnings

Changed existsFile to fileExists and existsDir to dirExists.

* Install packages during nimble develop
2020-11-25 11:28:12 +01:00
Xie Yanbo
40daae64e3
fix typo (#163) 2020-11-02 08:17:27 +01:00
Xie Yanbo
d3fc084c42
added documentation on rendering to string when using event handlers (#162) 2020-10-30 10:44:38 +01:00
Demian Florentin
bcab551f05
onwheel event added (#161) 2020-10-22 11:38:22 +02:00
Timothee Cour
04779f8502
add toCss: allows tdiv(style = "font-style: oblique; color: pink".toCss) (#158)
* add toCss: allows tdiv(style = "font-style: oblique; color: pink".toCss)

* fixup to work with nim c
2020-10-07 12:23:16 +02:00
Timothee Cour
bd18fc980c
fix #153 (#159) 2020-10-01 22:13:54 +02:00
Araq
5a2ea1eef9 1.1.3 2020-10-01 19:56:30 +02:00
Timothee Cour
d2f88b4648
close #6 (#157) 2020-10-01 19:35:25 +02:00
Timothee Cour
d8fe79fa40
make vstyles.setAttr kstring overload public (#156) 2020-10-01 09:28:42 +02:00
OctoCat
f0dd16ebfe
Fixed the error with password (#155)
* Fixed the error with password

* Changed to old style
2020-09-28 08:46:23 +02:00
Bung
0813a7adf9
add transition events (#151) 2020-08-26 18:03:52 +02:00
Bung
7a36b19729
watch ignore precompile style lang (#150) 2020-07-31 17:54:05 +02:00
Bung
98a2d1ce50
add static server,websocket refresh watching mode (#149)
* add static server,websocket refresh watching mode

* watch optional

* we dont quit when building error in watch mode

* staticDir,publicUrl configure through dot env

* remove unused import

* serve optional
2020-07-23 10:40:04 +02:00
tarunama
22e2e226cb
Refactor examples html, nim files. (#147)
* refactor: remove no need first new line

* arrange: add indent in HTML files

Co-authored-by: Takuya Yamamoto <takuya.yamamoto@tabian.co>
2020-07-02 10:36:20 +02:00
dawkot
f1cd9e606d
Added toSelected (#145)
Co-authored-by: dawkot <dawkot@github.com>
2020-05-02 13:42:53 +02:00
Miran
80f500874b
fix deprecated stuff (#144) 2020-04-27 10:59:54 +02:00
Araq
5f21dcdc56 1.1.2 2020-04-12 23:41:55 +02:00
Araq
02038d40c1 fixes #141 2020-04-12 23:41:24 +02:00
Araq
6b753009ac 1.1.1 2020-04-12 00:25:05 +02:00
Araq
d7da2a5bd5 fixes #139 2020-04-12 00:22:46 +02:00
Araq
c00d7dc340 Merge branch 'master' of github.com:pragmagic/karax 2020-04-01 17:07:00 +02:00
Araq
7f061de317 prepare for Nim version 1.2 2020-04-01 17:06:53 +02:00
hitoshihrm
0a7136b8fc
3 tiny changes for "All Done" button. (#136)
1."onAllDone" callback fixation.
Because the callback deletes all tasks instead of making them done, fixed.

2.Retouching checkbox and label property.
Because a checkbox didn't work and label did'nt affect the checkbox, I set
the callback on checkbox, and rename label's "for" property.

3.Adding Global var "doneswitch" for convinience.
I guess the button should be able to make "All Done" and "All Undone"
because the checkbox is not a button. So make it done, undone button
interfaced with the checkbox's display.
2020-03-12 09:23:07 +01:00
Bùi Thành Nhân
7440393d3a
Self-closing tags (#133) (#134) 2020-03-04 10:02:03 +01:00
Jairo
f6bda9ae89 Reactive value (#129)
Change proposed in https://github.com/pragmagic/karax/issues/61#issuecomment-522233192
2019-11-12 10:18:08 +01:00
Jairo
1911a1cfe0 Update karun.nim (#124)
Add common meta tag for Mobile first development
2019-10-10 17:23:45 +02:00
Timothee Cour
db4acbf7d6 fixes #121 (#122) 2019-09-30 09:01:07 +02:00
Andreas Rumpf
6a0c2871ad added an experimental avoidDomDiffing API; refs #121 2019-09-28 23:25:53 +02:00
Andreas Rumpf
9cf08c4f9c fixes a critical event handling regression 2019-09-27 23:04:17 +02:00
Araq
983cc8882b fixes #101 2019-09-26 21:19:15 +02:00
Araq
8dfb8727cd fixes #113 2019-09-26 21:16:11 +02:00
Araq
9396b3e69d Merge branch 'master' of github.com:pragmagic/karax 2019-09-26 21:01:54 +02:00
Timothee Cour
e6877a5198 fixes #119 (#120) 2019-09-21 23:18:05 +02:00
Timothee Cour
7a7cdd8e76 add onload (#116) 2019-09-21 23:17:41 +02:00
Dmitry Matveyev
023f5d2463 Fix typos and add small clarification to docs/docs.rst (#115) 2019-09-13 11:05:28 +02:00
Dmitry Matveyev
65e305e6e9 Fix script src path for html files to be up to date with readme (#114) 2019-09-09 11:15:19 +02:00
Araq
05766db4b6 Merge branch 'master' of github.com:pragmagic/karax 2019-09-02 12:33:23 +02:00
Araq
e422087217 avoid exponential DOM diffing 2019-08-28 19:38:20 +02:00
Araq
d08a0356d2 readme.rst: fixed a typo and made the snippet runnable 2019-08-28 19:38:20 +02:00
Araq
9c8a34e567 readme.rst: fixed a typo and made the snippet runnable 2019-08-15 11:07:03 +02:00
Jairo
1bbd9c694e Update karun.nim (#109)
Remove ugly debug echo (sorry)
2019-07-19 18:38:43 +02:00
Zed
b99a5430f0 Add support for rendering styles to string (#102) 2019-07-10 14:46:08 +02:00
Zed
fc062c2d48 Fix tag termination for elements with no children (#103) 2019-07-10 14:44:51 +02:00
Araq
5e9709bc5b fixes #91 2019-06-03 14:59:51 +02:00
Jairo
c091af1622 Update kdom_impl.nim (#94)
* Update kdom_impl.nim

Support full old syntax (maybe support modern syntax too? object options)
https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView

* Update kdom_impl.nim

use MDN naming

* Update kdom_impl.nim
2019-05-15 19:06:43 +02:00
Hitesh Jasani
33af725e7d Add docs explaining server side HTML rendering (#97)
* Add docs explaining server side HTML rendering

* Fix indentation
2019-05-15 19:06:17 +02:00
Araq
cd3e7cc1c8 fixed indentation 2019-05-15 18:48:38 +02:00
liuxiaodong
ad3c0a63ee update verbatim for static HTML (#96) 2019-05-15 18:36:22 +02:00
Andreas Rumpf
20fe35355c server side Karax rendering: show how events can be handled 2019-03-14 11:21:49 +01:00
Andreas Rumpf
50bed1f6e9 used sugar.nim instead of future.nim in the login.nim example 2019-03-14 11:09:06 +01:00
liuxiaodong
1ea7208b4d add uploadFile and a path tag (#88)
* add uploadFile and a path tag
* update uploadFile and circle tag
2019-03-10 20:15:13 +01:00
Jairo
4186a4b37b Update Bulma (#89) 2019-03-08 13:32:35 +01:00
Araq
0118aea506 use stdlib's dom module if possible 2019-01-31 21:28:31 +01:00
Araq
2936245d39 add newWebsocket variant that lacks the key 2019-01-31 16:16:37 +01:00
Araq
fc9f9312ef improve deprecation message 2019-01-31 16:15:03 +01:00
Araq
c8c7b13d2f update readme.rst, v1 has arrived 2018-12-12 15:18:36 +01:00
Araq
a2c2aac639 Karax v1 has arrived 2018-12-12 15:04:19 +01:00
Araq
18b5a12d1e added a new way for native DOM <-> virtual DOM interop, see nativenodes.nim example 2018-12-12 15:02:58 +01:00
Araq
a90efe752d merged #53 manually; todoapp must ignore empty input lines 2018-12-12 12:34:35 +01:00
Araq
068c30c97d make examples compile again with a config.nims file 2018-12-12 12:31:23 +01:00
Jairo
16008dbe52 Watch option (#79)
* Update karun.nim
* Watch option

Add a -w flag to watch for changes in folder and recompile.
2018-12-12 12:29:41 +01:00
Timothee Cour
b16112613a Update readme.rst (#77) 2018-10-10 00:43:08 +02:00
Timothee Cour
1ccd8b1c16 fix #73: echartstest.nim doesnt work (#75) 2018-09-28 09:24:39 +02:00
Andreas Rumpf
3a2effdf99 fixes readme.rst 2018-09-23 09:55:43 +02:00
Andreas Rumpf
fc0ca22785 update karax.nimble for latest Nimble 2018-09-23 09:52:22 +02:00
Araq
a07f1c7b82 fixes #72 2018-09-22 20:40:00 +02:00
LemonBoy
bd69de9f14 Don't choke on nnkDotExpr nodes (#71)
Fixes #69
2018-09-03 00:25:51 +02:00
Andreas Rumpf
1084849500 Merge branch 'master' of github.com:pragmagic/karax 2018-08-15 16:25:33 +02:00
Andreas Rumpf
b05c037bb0 Karax: prepare for the Nim release where 'nil' is not a thing for strings anymore 2018-08-15 16:25:26 +02:00
Dominik Picheta
b3a5ed9c54 Adds animation events. (#70) 2018-07-30 08:57:59 +02:00
Araq
015590e806 karax DSL: allow 'foo-bar' syntax for custom HTML attributes 2018-07-03 12:56:32 +02:00
Araq
6ab7c9455d make menus.nim example compile again 2018-07-03 12:56:05 +02:00
Araq
3b3aa6347d improve karax documentation 2018-07-03 12:19:19 +02:00
Araq
20fe1bcbee extended the readme.rst with plenty of examples from my blog post 2018-06-14 15:07:54 +02:00
Araq
fbfeced5b1 kajax.nim: add a switch to disable automatic redrawing when data was received 2018-06-14 15:07:34 +02:00
Andreas Rumpf
52026d1687 Karax DSL: allow 'style = {}' syntax for inline CSS 2018-06-10 13:27:58 +02:00
Andreas Rumpf
451ee92dd0 Merge branch 'master' of github.com:pragmagic/karax 2018-06-10 12:09:36 +02:00
Andreas Rumpf
330a7351f9 knete.nim: change order of parameters in replaceById 2018-06-10 12:09:30 +02:00
Araq
d8df257dd2 Merge branch 'master' of github.com:pragmagic/karax 2018-05-25 22:41:09 +02:00
Araq
092395ed5c bugfix: textareas are allowed to be weird 2018-05-25 22:41:01 +02:00
oskca
78cad87452 make --css works in karun (#64)
* make --css works in karun

* ignore karun executable in source tree
2018-05-25 09:12:22 +02:00
Araq
9882d1aec2 cleanup dom's PR 2018-05-24 00:01:01 +02:00
Dominik Picheta
9068686614 General improvements and fixes (#58)
* Fixes Nimble package structure.
* Improves readme.
* Fixes compatibility with devel.
* Add nice error message when ROOT is missing.
* Pass responseText instead of statusText in ajax callbacks.
* Implements verbatim node.
* Apparently it should just be true all the time.
* Small fix to readme.
* Fixes verbatim node updates.
* Move karun into the `karax` directory so that it's installable.
2018-05-23 23:30:06 +02:00
shinriyo
1e3a1e4745 add 'git clone' instructions to the readme (#51) 2018-03-14 12:23:39 +01:00
Andreas Rumpf
87edc4cdb9 fixes overly aggressive text node reuse DOM diff'ing bug 2018-03-07 11:52:25 +01:00
Andreas Rumpf
75739f8170 add experiment what Karax would look like without DOM diff'ing but with barebones reactive programming 2018-03-05 10:00:11 +01:00
Andreas Rumpf
dd0ea311a3 fixes an annoying karun bug 2018-03-05 09:58:39 +01:00
Araq
5576c4809d Karax: better support for routing; easier to use; todoapp bugfix: misapplied .compact macro 2018-02-17 14:13:26 +01:00
Andreas Rumpf
b36380a48e Merge branch 'master' of github.com:pragmagic/karax 2018-01-23 07:13:31 +01:00
Andreas Rumpf
db9325b3bb added karax.setForeignNodeId to make interfacing with ECharts easier 2018-01-23 07:12:47 +01:00
Andreas Rumpf
b5fc4de010 jjson: added the %* macro inspired from json.nim 2018-01-23 06:38:43 +01:00
Rayner De Los Santos F
7aace27355 Adding hasError method (#45) 2018-01-03 14:04:10 +01:00
Araq
271fdd9bde Merge branch 'darkmusic-master' 2017-12-12 15:13:11 +01:00
Araq
4428c4ed95 Merge branch 'master' of https://github.com/darkmusic/karax into darkmusic-master 2017-12-12 15:05:19 +01:00
Araq
135044d8d7 added vdom.getVNodeById for easier component creation 2017-12-05 16:32:11 +01:00
Andreas Rumpf
73c94c969c
Merge branch 'master' into master 2017-12-01 09:52:27 +01:00
Thomas Johnson
387c8b79fb
Merge branch 'master' into master 2017-11-30 11:52:18 -08:00
Araq
be759d9b42 bugfix: apply class changes consistently 2017-11-30 18:16:31 +01:00
Thomas Johnson
c9c30eaf45 Fixing spacing issue in kdom. (#43) 2017-11-26 15:20:01 -08:00
Thomas Johnson
22f24a1a8a Added blob support to ajaxPost. Cleanup in kdom and added InputElement and Blob support. Test updates. (#43) 2017-11-26 15:11:19 -08:00
Araq
1ad7567b28 added new experiments 2017-11-22 14:37:12 +01:00
Thomas Johnson
28bccf9223 Tab size fixes in domEventTests.css, .html for #43. 2017-11-21 17:14:59 -08:00
Thomas Johnson
b3100d5684 Tab size fixes in domEventTests for #43. 2017-11-21 17:01:23 -08:00
Thomas Johnson
d07fc9a8af Switched to non-minimized JQuery library for tests; code style updates to domEventTests for #43. 2017-11-21 16:52:38 -08:00
Araq
4571ad0dd5 merged PR #38 manually; refs #38 2017-11-21 23:44:45 +01:00
Andreas Rumpf
b8c9ff45fe added websockets' onopen event handler 2017-11-21 17:24:39 +01:00
Thomas Johnson
9ad41a6066 kdom changes and tests for #42 2017-11-20 17:12:33 -08:00
Araq
7425568122 attempt to make travis green again 2017-11-20 23:55:13 +01:00
Araq
84720812ea attempt to make travis green again 2017-11-20 23:47:06 +01:00
Araq
6c2513a48c attempt to make travis green again 2017-11-20 23:40:35 +01:00
Araq
22a20b3246 attempt to make travis green again 2017-11-20 23:33:56 +01:00
Araq
bf28a44439 fixes issues in the new examples 2017-11-20 14:04:34 +01:00
Araq
fb604321f3 added many more examples 2017-11-20 12:26:30 +01:00
Araq
bcc45feea3 added 'karun' tool to karax.nimble 2017-11-20 12:26:10 +01:00
Andreas Rumpf
e112f15092 tools/karun: produce CSS only with --css switch 2017-11-16 15:34:08 +01:00
Andreas Rumpf
a0665bc544 added errors.nim module for UI error handling 2017-11-13 15:27:58 +01:00
Andreas Rumpf
0ddf6098f7 improve performance for class-only changes; improves #40 but doesn't fix the real underlying combinatorial explosion 2017-10-31 09:40:52 +01:00
Andreas Rumpf
4be45499f7 improve karax.nimble 2017-10-10 12:59:38 +02:00
Andreas Rumpf
6ea55856b5 added JDict.keys iterator 2017-10-05 14:55:14 +02:00
Andreas Rumpf
6bd32a0e88 added toChecked and toDisabled helpers 2017-10-05 12:32:14 +02:00
Andreas Rumpf
9a8b020269 added 'kstrLits' macro 2017-10-05 12:29:52 +02:00
Andreas Rumpf
5bc983e0bf added lookuptables.nim helper module 2017-10-03 09:35:28 +02:00
Andreas Rumpf
8e8ada7cd2 move karax source code from 'src' to 'karax'; make karaxdsl work with native Nim; added jjson and jwebsockets modules 2017-10-02 22:54:25 +02:00
Andreas Rumpf
bb1732c834 make tests green again 2017-10-02 16:00:30 +02:00
Andreas Rumpf
57a6fca9d1 implement a autocomplete widget (work in progress) 2017-10-02 13:53:53 +02:00
Andreas Rumpf
312eb5180a added getVNodeById() for easier form handling 2017-10-02 13:52:49 +02:00
Andreas Rumpf
45951c96a4 added an example of how to do menus and auto-completion with Karax 2017-09-22 18:10:30 +02:00
Andreas Rumpf
55b551678a Merge branch 'master' of github.com:pragmagic/karax 2017-09-19 16:20:56 +02:00
Andreas Rumpf
34e2b8b7b4 added karax.runLater 2017-09-19 16:20:33 +02:00
Araq
a6a1d7e150 Merge branch 'master' into devel 2017-09-18 17:37:16 +02:00
konqoro
79d0b21cb3 Fix ending .} of pragmas (#39) 2017-09-17 18:16:30 +02:00
Araq
c324793f62 disable input text overwritings; needs further throughts 2017-09-06 16:36:27 +02:00
Andreas Rumpf
741ff50cae allow newRSeq[T]() as construction 2017-08-31 21:44:51 +02:00
Andreas Rumpf
2c3e8be9b4 support parameter-less event handlers 2017-08-29 19:32:49 +02:00
Andreas Rumpf
539f6d7244 use the generated getters to make the reactive extensions more robust 2017-08-29 10:53:16 +02:00
Andreas Rumpf
e70924c88e nextgen example now uses the makeReactive macro 2017-08-28 12:23:32 +02:00
Andreas Rumpf
db0bba2c3f change DSL: assume nkInfix nodes imply a void context 2017-08-28 12:15:58 +02:00
Andreas Rumpf
775a4a1289 nextgen example works now 2017-08-28 12:10:48 +02:00
Andreas Rumpf
f663d2f21b removed debugging code 2017-08-28 11:48:04 +02:00
Andreas Rumpf
4032bed306 created a nice 'makeReactive' macro 2017-08-28 11:29:43 +02:00
Andreas Rumpf
cb2dd6331d improvments to the new local diff algorithm 2017-08-28 11:29:12 +02:00
Andreas Rumpf
2482c772f8 more reactive stuff works now 2017-08-23 19:35:27 +02:00
Andreas Rumpf
9fce711bd6 wip; nothing works 2017-08-22 18:04:49 +02:00
Andreas Rumpf
56a50679de reactive sequence works now 2017-08-19 14:54:10 +02:00
Andreas Rumpf
46b1259498 added .track macro 2017-08-17 11:28:11 +02:00
Andreas Rumpf
bfb9985754 nextgen uses the karax DSL and DOM diffing 2017-08-17 08:36:17 +02:00
Andreas Rumpf
7c08c6bb76 first version; wip 2017-08-16 19:42:16 +02:00
Andreas Rumpf
bb846644b5 added support for a --define:karaxDebug switch 2017-08-08 13:29:29 +02:00
Araq
f9525dc646 added support for i18n (candidate for the stdlib?) 2017-07-14 16:17:06 +02:00
Andreas Rumpf
e94071b077 components work without 'key' attribute set 2017-07-12 13:23:58 +02:00
Andreas Rumpf
2ced690387 better event merging for components 2017-07-12 12:23:24 +02:00
Andreas Rumpf
e4d8cb4c20 make tests green again 2017-07-11 22:26:46 +02:00
Andreas Rumpf
b3c83ed119 optimized the diff algorithm further 2017-07-11 20:41:01 +02:00
Andreas Rumpf
507094bdfd better event handling for components and in the DSL 2017-07-11 18:59:38 +02:00
Andreas Rumpf
91d7ba25ea renamed key to 'index'; all virtual components have a 'key' attribute 2017-07-11 12:54:53 +02:00
Andreas Rumpf
5838271f8b nicer indentation 2017-07-11 12:43:32 +02:00
Andreas Rumpf
1e378b2ac2 separate component updating from diffing algorithm 2017-07-11 00:20:43 +02:00
Andreas Rumpf
6c152ed516 smart attribute comparisons 2017-07-10 21:42:22 +02:00
Andreas Rumpf
ec4a413a7b bugfix: better diff'ing for complex components 2017-07-10 18:00:35 +02:00
Andreas Rumpf
dc88eeaae3 make todoapp work again 2017-07-07 17:37:29 +02:00
Andreas Rumpf
920bd2db9d bugfix: component handling 2017-07-07 16:40:48 +02:00
Andreas Rumpf
dc7409e52e Merge branch 'stateful-components3' into devel 2017-07-06 20:26:51 +02:00
Andreas Rumpf
907c372c27 code cleanups 2017-07-06 18:53:07 +02:00
Andreas Rumpf
7779560b6b more tests and diff fixes; wip 2017-07-06 18:34:32 +02:00
Andreas Rumpf
a93e739081 new component system: blur.nim example now works 2017-07-05 16:07:52 +02:00
Andreas Rumpf
5f7f33bb02 wip 2017-07-04 08:04:04 +02:00
Andreas Rumpf
43ee890356 fix exponential blowup in diff algorith; wip 2017-07-03 23:03:30 +02:00
Araq
d538b7bc93 try to make components aware of the previous state 2017-07-02 09:34:58 +02:00
Andreas Rumpf
ea7eee7564 better input element handling 2017-06-29 17:10:29 +02:00
Andreas Rumpf
8e777a0c19 different detach handling to make event handlers work better 2017-06-29 12:25:08 +02:00
Andreas Rumpf
e739909466 rollback dom.value behaviour; fixes removeEventListener problem 2017-06-29 11:04:43 +02:00
Andreas Rumpf
8eef2074b4 make setFocus: bool work 2017-06-28 22:54:24 +02:00
Andreas Rumpf
c681f0055f update value property of real DOM nodes; minor DOM diff speedups; allow setFocus=false in the DSL 2017-06-28 15:23:19 +02:00
Andreas Rumpf
4199c42e3b combined components work 2017-06-28 12:56:51 +02:00
Andreas Rumpf
0ad9d68c81 fixes #35 2017-06-28 11:26:09 +02:00
Andreas Rumpf
ba88fb9a0b cancel redraw requests as an attempt to improve performance 2017-06-28 09:46:26 +02:00
Andreas Rumpf
c987323dba test nested components (still buggy) 2017-06-28 09:44:50 +02:00
Andreas Rumpf
fe7d72fa4e special treatment for input and textarea fields; minor code cleanups 2017-06-28 08:46:48 +02:00
Andreas Rumpf
418c027f6c smart-diff algorithm works now 2017-06-27 23:54:58 +02:00
Andreas Rumpf
8a1424669f optimized diff algorithm further; fixed a stupid bug; cleaned up the tests 2017-06-26 21:03:39 +02:00
Andreas Rumpf
09a5e1c34e optimized the new diff algorihtm 2017-06-24 08:23:22 +02:00
Andreas Rumpf
4bfe66edd6 optimized VStyle representation 2017-06-24 08:22:31 +02:00
Andreas Rumpf
74c07f0634 code cleanups 2017-06-23 22:21:06 +02:00
Andreas Rumpf
f89da765fa another bugfix for smart-diffing 2017-06-23 21:33:48 +02:00
Andreas Rumpf
64bc900456 rewritten diff algorithm 2017-06-23 21:03:31 +02:00
Andreas Rumpf
cd74cd9d4e another attempt to fix bug #31 2017-06-23 11:09:22 +02:00
Andreas Rumpf
9aa3ba545d wip 2017-06-22 21:21:54 +02:00
Andreas Rumpf
79270c3423 fixes #31 2017-06-22 09:34:44 +02:00
Andreas Rumpf
2d48975e97 Merge branch 'master' into stateful-components2 2017-06-22 09:17:24 +02:00
Andreas Rumpf
c8030bc185 diff algorithm also diffs components' expansions 2017-06-22 09:17:19 +02:00
Andreas Rumpf
051e9985b1 added 'karun' tool for convenient testing of karax apps 2017-06-21 21:08:20 +02:00
Pavel Nuzhdin
bd2d8e7d3f Added pointer-events style (#30) 2017-06-21 20:11:54 +02:00
Pavel Nuzhdin
8a2b893d5e Added onfocus (#29) 2017-06-21 20:09:52 +02:00
Andreas Rumpf
67ee246f40 fixes new rgba proc 2017-06-21 20:11:41 +02:00
Andreas Rumpf
994a51a1f0 Merge branch 'master' into stateful-components2 2017-06-21 18:54:41 +02:00
Andreas Rumpf
8dbc1da708 fixes #28 2017-06-21 18:20:57 +02:00
Andreas Rumpf
a535ad709b carousel is now a real example 2017-06-21 17:57:02 +02:00
Andreas Rumpf
d9648be60c sane markDirty() mechanism for components 2017-06-21 17:20:31 +02:00
Andreas Rumpf
ad0a3ffda9 Merge branch 'master' of github.com:pragmagic/karax 2017-06-21 08:21:19 +02:00
Andreas Rumpf
e58cbb78d2 fixes jstrutils.isInt 2017-06-21 08:21:05 +02:00
Andreas Rumpf
c86916e37d stateful components now in a workable state; renamed .component macro to .compact which much better captures its meaning 2017-06-20 00:14:54 +02:00
Andreas Rumpf
4801b8d8c1 new component example works 2017-06-19 21:25:34 +02:00
Araq
d1e0e1367b wip 2017-06-18 18:01:48 +02:00
Fabian Keller
b189b24a4f fixes #25 (#26) 2017-06-18 00:01:27 +02:00
Andreas Rumpf
2c05c772ca Merge pull request #27 from bluenote10/added_travis_link
Added travis link
2017-06-18 00:00:05 +02:00
Fabian Keller
972fcb0950 proper rst solution 2017-06-17 17:05:37 +02:00
Fabian Keller
7b246d0d40 added travis link 2017-06-17 16:57:36 +02:00
Araq
e68d92d22a Merge branch 'draft/refs' of https://github.com/bluenote10/karax into bluenote10-draft/refs 2017-06-17 00:00:38 +02:00
Araq
28c4f4b76a update kajax.nim to use the 'kxi' context too 2017-06-16 22:57:32 +02:00
Araq
b3fba429ad introduce KaraxInstance; 100% backwards compatible 2017-06-16 18:02:35 +02:00
Araq
08538f7f26 move parseInt to jstrutils.nim to avoid conflicts; introduce travis testing 2017-06-16 16:45:06 +02:00
Fabian Keller
e33b9dc09a added base methods for VNodeRef 2017-06-15 19:01:15 +02:00
Fabian Keller
472f1deb58 cleaned up example 2017-06-15 14:38:39 +02:00
Fabian Keller
14660f9d68 implemented refs draft 2017-06-15 13:53:53 +02:00
Andreas Rumpf
45931c5e4a Merge pull request #16 from bluenote10/feature/post_render_callback
Enable list transitions
2017-06-10 10:37:51 +02:00
Fabian Keller
d5596c8ff5 added post render callback 2017-06-09 19:10:21 +02:00
Andreas Rumpf
3277f19503 Merge pull request #13 from alehander42/vdom-submit-form
Support submit and input event in the vdom
2017-06-09 12:14:28 +02:00
Andreas Rumpf
f9460e6e05 Merge pull request #22 from pragmagic/feature/stop-propagation
Added stopPropagation event method
2017-06-09 11:07:06 +02:00
Pavel Nuzhdin
dc5ba1f646 Added stopPropagation event method 2017-06-09 15:44:16 +07:00
Andreas Rumpf
f17e37ac95 Merge branch 'master' into smart-vstyle-diff 2017-06-07 12:35:21 +02:00
Andreas Rumpf
e49fa7edf7 disable smart vstyle diffing for now 2017-06-07 10:07:37 +02:00
Andreas Rumpf
18d0187228 Merge pull request #21 from bluenote10/feature/all_css_styles
added all css styles to close #20
2017-06-07 08:28:51 +02:00
Fabian Keller
fca6b2a6ca added all css styles to close #20 2017-06-06 20:02:46 +02:00
Andreas Rumpf
ed3d467f31 diff algorithm bugfix: don't be too smart in key handling 2017-06-05 23:20:22 +02:00
Andreas Rumpf
22f458724f new diff algorithm works 2017-06-05 21:31:26 +02:00
Andreas Rumpf
8a639f3121 stuff 2017-06-05 17:06:58 +02:00
Andreas Rumpf
06f5283709 vstyle diff: performance improvement 2017-06-05 10:25:46 +02:00
Andreas Rumpf
a9b5216a42 diff algorithm special cases diffing of virtual styles 2017-06-05 10:21:38 +02:00
Andreas Rumpf
c895d18ccb renamed dom module to 'kdom' to avoid conflicts with Nim's stdlib dom module; it will further diverge from it anyway 2017-05-29 10:59:59 +02:00
Araq
d6c7be7106 added vstyles karax module for inline CSS support 2017-05-18 15:53:58 +02:00
Alexander Ivanov
76a2031c37 Support submit and input event in the vdom 2017-05-18 12:58:43 +03:00
Andreas Rumpf
68134abe3f Merge pull request #12 from alehander42/fix-17
Fix some issues with v0.17
2017-05-18 10:12:11 +02:00
Alexander Ivanov
084a1988c8 Resolve a macro bug 2017-05-18 10:01:02 +03:00
Alexander Ivanov
9160776999 Resolve a quote bug 2017-05-18 10:00:39 +03:00
Andreas Rumpf
20bf4a31d5 Merge pull request #8 from freevryheid/master
Add oncontextmenu event
2017-05-15 21:35:00 +02:00
Andre Smit
28f3e61b2e Add oncontextmenu event 2017-05-15 09:46:38 -05:00
Andreas Rumpf
1f0c5d98dd Update Untitled Diagram.xml 2017-05-13 13:16:19 +02:00
Andreas Rumpf
b44f699dd0 Update Untitled Diagram.xml 2017-05-13 13:09:27 +02:00
Andreas Rumpf
521ef9baec Update Untitled Diagram.xml 2017-05-13 13:01:50 +02:00
Andreas Rumpf
63c7cbc43b test_diagram.xml 2017-05-13 12:57:53 +02:00
Andreas Rumpf
21c911ffd3 added karaxdsl.flatHtml macro 2017-04-26 14:51:09 +02:00
Andreas Rumpf
2d1e656061 better setupErrorHandler 2017-04-26 14:46:01 +02:00
Andreas Rumpf
8666d8d6d6 kajax: PUT should have been POST 2017-04-24 17:52:29 +02:00
Andreas Rumpf
23f00055a9 renamed ajax.nim to kajax.nim 2017-04-24 17:46:37 +02:00
Andreas Rumpf
cf9b22ae62 bugfix: make ajax module work 2017-04-24 17:45:40 +02:00
Andreas Rumpf
30491d401a update karax.nimble file 2017-04-24 16:27:20 +02:00
Andreas Rumpf
b1e60fbd21 readme.rst: changed the wording slightly 2017-04-22 10:56:14 +02:00
Andreas Rumpf
a9c4b7ab67 add benchmark result 2017-04-22 10:55:28 +02:00
Andreas Rumpf
6aa87e4b81 todoapp: use local storage properly 2017-04-22 10:45:42 +02:00
Andreas Rumpf
00354cfc18 Merge pull request #3 from pragmagic/todoapp_with_local_storage
Todoapp with local storage
2017-04-22 09:16:10 +02:00
Roman Vishnevskii
479db8c2cc Codestyle fixes 2017-04-22 13:33:20 +07:00
Roman Vishnevskii
459d696128 Codestyle fixes 2017-04-22 13:29:24 +07:00
Roman Vishnevskii
3238422412 Add local storage for todoapp 2017-04-22 13:01:05 +07:00
Andreas Rumpf
7d3224183a completed first version of the Ajax API 2017-04-21 18:58:03 +02:00
Andreas Rumpf
27cacbd271 moved ajax support into its own module and make the callback call 'redraw' automatically 2017-04-21 18:30:02 +02:00
Andreas Rumpf
037ddef91d experiments: added an animation to the example 2017-04-21 18:15:50 +02:00
Andreas Rumpf
371ae67c64 fixes a bug in the components DSL tranformation 2017-04-21 18:15:12 +02:00
Andreas Rumpf
e21b4fa6a1 attempt to optimize todoapp wiht .component annotation 2017-04-21 12:49:37 +02:00
Andreas Rumpf
2c0dad5094 Merge branch 'stateful-components' 2017-04-19 11:26:08 +02:00
Andreas Rumpf
be4159fc11 stateful components now work 2017-04-19 11:17:00 +02:00
Andrey Sobolev
549a643fb9 Update readme.rst 2017-04-19 09:48:28 +07:00
Andreas Rumpf
c74b90876d make playerapp compile again 2017-04-18 10:46:15 +02:00
Araq
f0e2d41fde next step to get stateful components 2017-04-17 09:39:13 +02:00
Araq
0255f58964 wip: stateful components 2017-04-16 09:51:10 +02:00
Araq
5f083a7d01 bugfix: virtual thunks need to have a key of -1 2017-04-16 01:36:42 +02:00
Andreas Rumpf
686ad23f2f reworked event handling again; added scrollapp example 2017-04-15 15:50:07 +02:00
Andreas Rumpf
5bc7d4e336 removed the need to call karax.init explicitly 2017-04-15 11:50:05 +02:00
Andreas Rumpf
d9bb8d99e8 made readme.rst less confusing 2017-04-15 11:18:43 +02:00
Andreas Rumpf
5bd43f13f9 added code snippet 2017-04-15 11:18:15 +02:00
Andreas Rumpf
cfed5e05ec added more event kinds 2017-04-15 11:17:12 +02:00
Andreas Rumpf
546162c388 added what's left to do 2017-04-14 20:59:25 +02:00
Andreas Rumpf
3f14cdcf0d added missing files 2017-04-14 20:51:37 +02:00
Andreas Rumpf
a9b12a231f fixed components; added playerapp example 2017-04-14 20:49:42 +02:00
Andreas Rumpf
5d71662617 fixed .component macro 2017-04-14 20:33:03 +02:00
Andreas Rumpf
199408f160 wrote more documentation 2017-04-14 19:39:18 +02:00
Andreas Rumpf
74c5fb48ff added support for all HTML5 tags and drag&drop events 2017-04-14 19:39:00 +02:00
Andreas Rumpf
be0f9c9e08 added support for all HTML5 tags and drag&drop events 2017-04-14 19:36:16 +02:00
Andreas Rumpf
21bb6a0311 readme.rst: better english 2017-04-13 21:22:57 +02:00
Andreas Rumpf
a799413f7f added .component macro to karax 2017-04-13 21:07:04 +02:00
95 changed files with 19646 additions and 1851 deletions

31
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,31 @@
# This is a basic workflow to help you get started with Actions
name: CI
# Controls when the action will run.
on:
# Triggers the workflow on push or pull request events but only for the master branch
push:
branches: [ master ]
pull_request:
branches: [ master ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
container: nimlang/nim
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
# Runs a single command using the runners shell
- name: Tests
run: |
nimble develop -y
nim c -r tests/tester.nim

6
.gitignore vendored
View file

@ -1 +1,7 @@
nimcache/
/tests/tester
karax/tools/karun
*.code-workspace
*.exe
app.js
app.html

21
LICENSE.txt Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2017 Xored Software, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

BIN
docs/benchmark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

58
docs/docs.rst Normal file
View file

@ -0,0 +1,58 @@
Virtual DOM
===========
The virtual dom is in the ``vdom`` module.
The ``VNodeKind`` enum describes every tag that the HTML 5 spec includes
and some extensions that allow for an efficient component system.
These extensions are:
``VNodeKind.int``
The node has a single integer field.
``VNodeKind.bool``
The node has a single boolean field.
``VNodeKind.vthunk``
The node is a `virtual thunk`:idx:. This means there is a
function attached to it that produces the ``VNode`` structure
on demand.
``VNodeKind.dthunk``
The node is a `DOM thunk`:idx:. This means there is a
function attached to it that produces the ``Node`` DOM structure
on demand.
For convenience of the resulting Nim DSL these tags have enum names
that differ from their HTML equivalent:
================= =======================================================
Enum value HTML
================= =======================================================
``tdiv`` ``div`` (changed because ``div`` is a keyword in Nim)
``italic`` ``i``
``bold`` ``b``
``strikethrough`` ``s``
``underlined`` ``u``
================= =======================================================
A virtual dom node also has a special field set called ``key``, an integer
that can be used as a data model specific key/id. It can be accessed in event
handlers to change the data model. See the todoapp for an example of how to
use it.
**Note**: This is not a hint for the DOM diff algorithm, multipe nodes can
all have the same key value.
Event system
============
Karax does not abstract over the event system the DOM offers much: The same
``Event`` structure is used. Every callback has the
signature ``proc (ev: Event; n: VNode)`` or empty ``proc ()``.

15
examples/button.nim Normal file
View file

@ -0,0 +1,15 @@
include karax / prelude
var lines: seq[kstring] = @[]
proc createDom(): VNode =
result = buildHtml(tdiv):
button:
text "Say hello!"
proc onclick(ev: Event; n: VNode) =
lines.add "Hello simulated universe"
for x in lines:
tdiv:
text x
setRenderer createDom

14
examples/buttonlambda.nim Normal file
View file

@ -0,0 +1,14 @@
include karax / prelude
from future import `=>`
var lines: seq[kstring] = @[]
proc createDom(): VNode =
result = buildHtml(tdiv):
button(onclick = () => lines.add "Hello simulated universe"):
text "Say hello!"
for x in lines:
tdiv:
text x
setRenderer createDom

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Carousel app</title>
</head>
<body id="body">
<div id="ROOT" />
<script type="text/javascript" src="carousel.js"></script>
</body>
</html>

View file

@ -0,0 +1,69 @@
## This demo shows how you can develop your own stateful components with Karax.
import vdom, vstyles, karax, karaxdsl, jdict, jstrutils, kdom
type
Carousel = ref object of VComponent
counter: int
cntdown: int
timer: TimeOut
list: seq[cstring]
const ticksUntilChange = 5
var
images: seq[cstring] = @[cstring"a", "b", "c", "d"]
proc render(x: VComponent): VNode =
let self = Carousel(x)
proc docount() =
dec self.cntdown
if self.cntdown == 0:
self.counter = (self.counter + 1) mod self.list.len
self.cntdown = ticksUntilChange
else:
self.timer = setTimeout(docount, 30)
markDirty(self)
redraw()
proc onclick(ev: Event; n: VNode) =
if self.timer != nil:
clearTimeout(self.timer)
self.timer = setTimeout(docount, 30)
let col =
case self.counter
of 0: cstring"#4d4d4d"
of 1: cstring"#ff00ff"
of 2: cstring"#00ffff"
of 3: cstring"#ffff00"
else: cstring"red"
result = buildHtml(tdiv()):
text self.list[self.counter]
button(onclick = onclick):
text "Next"
tdiv(style = style(StyleAttr.color, col)):
text "This changes its color."
if self.cntdown != ticksUntilChange:
text &self.cntdown
proc carousel(): Carousel =
result = newComponent(Carousel, render)
result.list = images
result.cntdown = ticksUntilChange
proc createDom(): VNode =
result = buildHtml(table):
tr:
td:
carousel()
td:
carousel()
tr:
td:
carousel()
td:
carousel()
setRenderer createDom

View file

@ -0,0 +1 @@
--path: "../../karax"

1
examples/config.nims Normal file
View file

@ -0,0 +1 @@
--path: ".."

17
examples/hellostyle.nim Normal file
View file

@ -0,0 +1,17 @@
include karax / prelude
import karax / vstyles
proc createDom(): VNode =
result = buildHtml(tdiv):
tdiv(style = style(StyleAttr.color, "red".cstring)):
text "red"
tdiv(style = style(StyleAttr.color, "blue")):
text "blue"
# explicit `kstring` required for varargs overload
tdiv(style = style((fontStyle, "italic".kstring), (color, "orange".kstring))):
text "italic orange"
# can use a string directly
tdiv(style = "font-style: oblique; color: pink".toCss):
text "oblique pink"
setRenderer createDom

View file

@ -0,0 +1,12 @@
include karax / prelude
import random
proc createDom(): VNode =
result = buildHtml(tdiv):
if rand(100) <= 50:
text "Hello World!"
else:
text "Hello Universe"
randomize()
setRenderer createDom

7
examples/helloworld.nim Normal file
View file

@ -0,0 +1,7 @@
include karax / prelude
proc createDom(): VNode =
result = buildHtml(tdiv):
text "Hello World!"
setRenderer createDom

46
examples/login.nim Normal file
View file

@ -0,0 +1,46 @@
include karax / prelude
from sugar import `=>`
import karax / errors
proc loginField(desc, field, class: kstring;
validator: proc (field: kstring): proc ()): VNode =
result = buildHtml(tdiv):
label(`for` = field):
text desc
input(class = class, id = field, onchange = validator(field))
# some consts in order to prevent typos:
const
username = kstring"username"
password = kstring"password"
proc validateNotEmpty(field: kstring): proc () =
result = proc () =
let x = getVNodeById(field)
if x.text == "":
errors.setError(field, field & " must not be empty")
else:
errors.setError(field, "")
var loggedIn: bool
proc loginDialog(): VNode =
result = buildHtml(tdiv):
if not loggedIn:
loginField("Name: ", username, "input", validateNotEmpty)
loginField("Password: ", password, "password", validateNotEmpty)
button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()):
text "Login"
p:
text errors.getError(username)
p:
text errors.getError(password)
else:
p:
text "You are now logged in."
setError username, username & " must not be empty"
setError password, password & " must not be empty"
when not declared(toychat):
setRenderer loginDialog

View file

@ -0,0 +1,42 @@
import karax / [karax, karaxdsl, vdom, kdom, compact]
const
Play = 0
Big = 1
Small = 2
Normal = 3
proc paused(n: Node): bool {.importcpp: "#.paused".}
proc play(n: Node) {.importcpp.}
proc pause(n: Node) {.importcpp.}
proc `width=`(n: Node, w: int) {.importcpp: "#.width = #".}
proc mplayer*(id, resource: cstring): VNode {.compact.} =
proc handler(ev: Event; n: VNode) =
let myVideo = document.getElementById(id)
case n.index
of Play:
if myVideo.paused:
myVideo.play()
else:
myVideo.pause()
of Big: myVideo.width = 560
of Small: myVideo.width = 320
of Normal: myVideo.width = 420
else: discard
result = buildHtml(tdiv):
button(onclick=handler, index=Play):
text "Play/Pause"
button(onclick=handler, index=Big):
text "Big"
button(onclick=handler, index=Small):
text "Small"
button(onclick=handler, index=Normal):
text "Normal"
br()
br()
video(id=id, width="420"):
source(src=resource, `type`="video/mp4"):
text "Your browser does not support HTML5 video."

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Player app</title>
</head>
<body id="body">
<div id="ROOT" />
<script type="text/javascript" src="playerapp.js"></script>
</body>
</html>

View file

@ -0,0 +1,21 @@
## Example app that shows how to write and embed a custom component.
include karax/prelude
import mediaplayer
const url = "https://www.w3schools.com/html/mov_bbb.mp4"
proc createDom(): VNode =
result = buildHtml(table):
tr:
td:
mplayer("vid1", url)
td:
mplayer("vid2", url)
tr:
td:
mplayer("vid3", url)
td:
mplayer("vid4", url)
setRenderer createDom

View file

@ -0,0 +1 @@
--path: "../.."

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Todo app</title>
</head>
<body id="body">
<div id="ROOT" />
<script type="text/javascript" src="scrollapp.js"></script>
</body>
</html>

View file

@ -0,0 +1,25 @@
## Example that shows how to accomplish an "infinitely scrolling" app.
include karax/prelude
import karax / [jstrutils, kdom, vstyles]
var entries: seq[cstring] = @[]
for i in 1..500:
entries.add(cstring("Entry ") & &i)
proc scrollEvent(ev: Event; n: VNode) =
let d = n.dom
if d != nil and inViewport(d.lastChild):
# "load" more data:
for i in 1..50:
entries.add(cstring("Loaded Entry ") & &i)
proc createDom(): VNode =
result = buildHtml():
tdiv(onscroll=scrollEvent, style=style(
(StyleAttr.height, cstring"400px"), (StyleAttr.overflow, cstring"scroll"))):
for x in entries:
tdiv:
text x
setRenderer createDom

View file

@ -0,0 +1 @@
--path: "../.."

View file

@ -1,19 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<title>Todo app</title>
<link rel="stylesheet" href="style.css">
</head>
<head>
<title>Todo app</title>
<link rel="stylesheet" href="style.css">
</head>
<body id="body">
<body id="body">
<div id="ROOT" />
<div id="ROOT" />
<script type="text/javascript" src="nimcache/todoapp.js"></script>
<script type="text/javascript">
onload()
</script>
<script type="text/javascript" src="todoapp.js"></script>
</body>
</body>
</html>

View file

@ -1,116 +1,152 @@
import vdom, karax, karaxdsl, jstrutils
import karax / [vdom, karax, karaxdsl, jstrutils, compact, localstorage]
type
Filter = enum
all, active, completed
var
entries: seq[(cstring, bool)]
selectedEntry = -1
filter: Filter
entriesLen: int
doneswitch = true
const
contentSuffix = cstring"content"
completedSuffix = cstring"completed"
lenSuffix = cstring"entriesLen"
proc getEntryContent(pos: int): cstring =
result = getItem(&pos & contentSuffix)
if result == cstring"null":
result = nil
proc isCompleted(pos: int): bool =
var value = getItem(&pos & completedSuffix)
result = value == cstring"true"
proc setEntryContent(pos: int, content: cstring) =
setItem(&pos & contentSuffix, content)
proc markAsCompleted(pos: int, completed: bool) =
setItem(&pos & completedSuffix, &completed)
proc addEntry(content: cstring, completed: bool) =
setEntryContent(entriesLen, content)
markAsCompleted(entriesLen, completed)
inc entriesLen
setItem(lenSuffix, &entriesLen)
proc updateEntry(pos: int, content: cstring, completed: bool) =
setEntryContent(pos, content)
markAsCompleted(pos, completed)
proc onTodoEnter(ev: Event; n: VNode) =
entries.add((n.value, false))
n.value = ""
if n.value.strip() != "":
addEntry(n.value, false)
n.value = ""
proc removeHandler(ev: Event; n: VNode) =
entries[n.key] = (cstring(nil), false)
updateEntry(n.index, cstring(nil), false)
proc editHandler(ev: Event; n: VNode) =
selectedEntry = n.key
selectedEntry = n.index
proc focusLost(ev: Event; n: VNode) = selectedEntry = -1
proc editEntry(ev: Event; n: VNode) =
entries[n.key][0] = n.value
setEntryContent(n.index, n.value)
selectedEntry = -1
proc toggleEntry(ev: Event; n: VNode) =
let id = n.key
entries[id][1] = not entries[id][1]
let id = n.index
markAsCompleted(id, not isCompleted(id))
proc onAllDone(ev: Event; n: VNode) =
entries = @[]
selectedEntry = -1
for i in 0..<entriesLen:
markAsCompleted(i, doneswitch)
doneswitch = not doneswitch
proc clearCompleted(ev: Event, n: VNode) =
for i in 0..<entries.len:
if entries[i][1]: entries[i][0] = nil
for i in 0..<entriesLen:
if isCompleted(i): setEntryContent(i, nil)
proc toClass(completed: bool): cstring =
(if completed: cstring"completed" else: cstring(nil))
proc toChecked(checked: bool): cstring =
(if checked: cstring"checked" else: cstring(nil))
proc selected(v: Filter): cstring =
(if filter == v: cstring"selected" else: cstring(nil))
proc createEntry(id: int; d: cstring; completed, selected: bool): VNode =
proc createEntry(id: int; d: cstring; completed, selected: bool): VNode {.compact.} =
result = buildHtml(tr):
li(class=toClass(completed)):
if not selected:
tdiv(class = "view"):
input(class = "toggle", `type` = "checkbox", checked = toChecked(completed),
onclick=toggleEntry, key=id)
label(onDblClick=editHandler, key=id):
onclick=toggleEntry, index=id)
label(onDblClick=editHandler, index=id):
text d
button(class = "destroy", key=id, onclick=removeHandler)
button(class = "destroy", index=id, onclick=removeHandler)
else:
input(class = "edit", name = "title", key=id,
onfocusLost = focusLost,
onenter = editEntry, value = d, setFocus)
input(class = "edit", name = "title", index=id,
onblur = focusLost,
onkeyupenter = editEntry, value = d, setFocus=true)
proc createDom(): VNode =
proc makeFooter(entriesCount, completedCount: int): VNode =
result = buildHtml(footer(class = "footer")):
span(class = "todo-count"):
strong:
text(&entriesCount)
text cstring" item" & &(if entriesCount != 1: "s left" else: " left")
ul(class = "filters"):
li:
a(class = selected(all), href = "#/"):
text "All"
li:
a(class = selected(active), href = "#/active"):
text "Active"
li:
a(class = selected(completed), href = "#/completed"):
text "Completed"
button(class = "clear-completed", onclick = clearCompleted):
text "Clear completed (" & &completedCount & ")"
proc makeHeader(): VNode {.compact.} =
result = buildHtml(header(class = "header")):
h1:
text "todos"
input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
onkeyupenter = onTodoEnter, setFocus)
proc createDom(data: RouterData): VNode =
if data.hashPart == "#/": filter = all
elif data.hashPart == "#/completed": filter = completed
elif data.hashPart == "#/active": filter = active
result = buildHtml(tdiv(class="todomvc-wrapper")):
section(class = "todoapp"):
header(class = "header"):
h1:
text "todos"
input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
onenter = onTodoEnter, setFocus)
makeHeader()
section(class = "main"):
input(class = "toggle-all", `type` = "checkbox", name = "toggle")
label(`for` = "toggle-all", onclick = onAllDone):
input(class = "toggle-all", `type` = "checkbox", id = "toggle", onclick = onAllDone)
label(`for` = "toggle"):
text "Mark all as complete"
var entriesCount = 0
var completedCount = 0
ul(class = "todo-list"):
for i, d in pairs(entries):
if d[0] != nil:
#for i, d in pairs(entries):
for i in 0..entriesLen-1:
var d0 = getEntryContent(i)
var d1 = isCompleted(i)
if d0 != nil:
let b = case filter
of all: true
of active: not d[1]
of completed: d[1]
of active: not d1
of completed: d1
if b:
createEntry(i, d[0], d[1], i == selectedEntry)
inc completedCount, ord(d[1])
createEntry(i, d0, d1, i == selectedEntry)
inc completedCount, ord(d1)
inc entriesCount
footer(class = "footer"):
span(class = "todo-count"):
strong:
text(&entriesCount)
text cstring" item" & &(if entriesCount != 1: "s left" else: " left")
ul(class = "filters"):
li:
anchor(class = selected(all), href = "#/"):
text "All"
li:
anchor(class = selected(active), href = "#/active"):
text "Active"
li:
anchor(class = selected(completed), href = "#/completed"):
text "Completed"
button(class = "clear-completed", onclick = clearCompleted):
text "Clear completed (" & &completedCount & ")"
makeFooter(entriesCount, completedCount)
setOnHashChange(proc(hash: cstring) =
if hash == cstring"#/": filter = all
elif hash == cstring"#/completed": filter = completed
elif hash == cstring"#/active": filter = active
)
if hasItem(lenSuffix):
entriesLen = parseInt getItem(lenSuffix)
else:
entriesLen = 0
setRenderer createDom
proc onload() {.exportc.} =
init()

View file

@ -1 +1 @@
--path: "../../src"
--path: "../.."

35
examples/toychat.nim Normal file
View file

@ -0,0 +1,35 @@
include login
const
message = "message"
type
TextMessage = ref object
name, content: kstring
var allMessages: seq[TextMessage] = @[]
proc doSendMessage() =
let inputField = getVNodeById(message)
allMessages.add(TextMessage(name: "you", content: inputField.getInputText))
inputField.setInputText ""
proc main(): VNode =
result = buildHtml(tdiv):
loginDialog()
tdiv:
table:
for m in allMessages:
tr:
td:
bold:
text m.name
td:
text m.content
tdiv:
if loggedIn:
label(`for` = message):
text "Message: "
input(class = "input", id = message, onkeyupenter = doSendMessage)
setRenderer main

13
experiments/ajaxtest.html Normal file
View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>Karax AJAX Test</title>
</head>
<body id="body">
<div id="ROOT" />
<script type="text/javascript" src="ajaxtest.js"></script>
</html>

6
experiments/ajaxtest.nim Normal file
View file

@ -0,0 +1,6 @@
import kdom, kajax
proc cb(httpStatus: int, response: cstring) =
echo "Worked!"
ajaxGet("https://httpbin.org/get", @[], cb)

2
experiments/config.nims Normal file
View file

@ -0,0 +1,2 @@
--path: "../karax"
--path: ".."

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title>Karax ECharts Test</title>
</head>
<body id="body">
<div id="ROOT" />
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/echarts/4.0.2/echarts-en.js"></script>
<script type="text/javascript" src="echartstest.js"></script>
</html>

View file

@ -0,0 +1,58 @@
import karax / [kbase, vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, jjson]
type
Views = enum
Customers, Products
var
currentView = Customers
type
EChart* = ref object
proc echartsInit(n: Element): EChart {.importc: "echarts.init".}
proc setOption(x: EChart; option: JsonNode) {.importcpp.}
proc postRender(data: RouterData) =
if currentView == Products:
let myChart = echartsInit(kdom.getElementById("echartSection"))
# specify chart configuration item and data
let option = %*{
"title": {
"text": "ECharts entry example"
},
"tooltip": {},
"legend": {
"data": ["Sales"]
},
"xAxis": {
"data": ["shirt","cardign","chiffon shirt","pants","heels","socks"]
},
"yAxis": {},
"series": [{
"name": "Sales",
"type": "bar",
"data": [5, 20, 36, 10, 10, 20]
}]
}
myChart.setOption(option)
proc createDom(data: RouterData): VNode =
let hash = data.hashPart
if hash == cstring"#/Products": currentView = Products
else: currentView = Customers
result = buildHtml(tdiv):
ul(class = "tabs"):
for v in low(Views)..high(Views):
li:
a(href = "#/" & $v):
text cstring($v)
if currentView == Products:
tdiv(id = "echartSection", style = style((width, kstring"600px"), (height, kstring"400px")))
tdiv:
text "other section"
setRenderer createDom, "ROOT", postRender
setForeignNodeId "echartSection"

View file

@ -1,19 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Todo app</title>
<link rel="stylesheet" href="style.css">
</head>
<body id="body">
<div id="ROOT" />
<script type="text/javascript" src="nimcache/example.js"></script>
<script type="text/javascript">
onload(window.location.pathname.split('/')[1])
</script>
</body>
</html>

View file

@ -1,135 +0,0 @@
import vdom, karax, karaxdsl, jdict, jstrutils
type
Filter = enum
all, active, completed
var
entries: seq[(cstring, bool)]
selectedEntry = -1
filter: Filter
proc onTodoEnter(ev: Event; n: VNode) =
entries.add((n.value, false))
n.value = ""
proc removeHandler(ev: Event; n: VNode) =
let id = suffixAsInt(n.id, "remove:")
entries[id] = (cstring(nil), false)
proc editHandler(ev: Event; n: VNode) =
let id = suffixAsInt(n.id, "edit:")
selectedEntry = id
when defined(usecache):
var entryCache = newJDict[int, VNode]()
proc focusLost(ev: Event; n: VNode) = selectedEntry = -1
proc editEntry(ev: Event; n: VNode) =
let id = suffixAsInt(n.id, "todo-edit:")
entries[id][0] = n.value
selectedEntry = -1
proc toggleEntry(ev: Event; n: VNode) =
let id = suffixAsInt(n.id, "toggle:")
entries[id][1] = not entries[id][1]
proc onAllDone(ev: Event; n: VNode) =
entries = @[]
selectedEntry = -1
proc clearCompleted(ev: Event, n: VNode) =
for i in 0..<entries.len:
if entries[i][1]: entries[i][0] = nil
proc toClass(completed: bool): cstring =
(if completed: cstring"completed" else: cstring"")
proc toVis(visible: bool): cstring =
(if visible: cstring"visible" else: "hidden")
proc toChecked(checked: bool): cstring =
(if checked: cstring"checked" else: cstring(nil))
proc selected(v: Filter): cstring =
(if filter == v: cstring"selected" else: cstring(nil))
proc createEntry(id: int; d: cstring; completed, selected: bool): VNode =
# implement caching:
when defined(usecache):
if entryCache.contains(i):
let old = entryCache[i]
return old
result = buildHtml(tr):
li(class=toClass(completed)):
if not selected:
tdiv(class = "view"):
input(class = "toggle", `type` = "checkbox", checked = toChecked(completed),
onclick=toggleEntry, id="toggle:" & &id)
label(onDblClick=editHandler, id="edit:" & &id):
text d
button(class = "destroy", id="remove:" & $id, onclick=removeHandler)
else:
input(class = "edit", name = "title", id = "todo-edit:" & &id,
onfocusLost = focusLost,
onenter = editEntry, value = d)
when defined(usecache):
entryCache[i] = result
proc createDom(): VNode =
result = buildHtml(tdiv(class="todomvc-wrapper")):
section(class = "todoapp"):
header(class = "header"):
h1:
text "todos"
input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
onenter = onTodoEnter)
section(class = "main"):
input(class = "toggle-all", `type` = "checkbox", name = "toggle")
label(`for` = "toggle-all", onclick = onAllDone):
text "Mark all as complete"
var entriesCount = 0
var completedCount = 0
ul(class = "todo-list"):
for i, d in pairs(entries):
if d[0] != nil:
let b = case filter
of all: true
of active: not d[1]
of completed: d[1]
if b:
createEntry(i, d[0], d[1], i == selectedEntry)
inc completedCount, ord(d[1])
inc entriesCount
footer(class = "footer"):
span(class = "todo-count"):
strong:
text(&entriesCount)
text cstring" item" & &(if entriesCount != 1: "s left" else: " left")
ul(class = "filters"):
li:
anchor(class = selected(all), href = "#/"):
text "All"
li:
anchor(class = selected(active), href = "#/active"):
text "Active"
li:
anchor(class = selected(completed), href = "#/completed"):
text "Completed"
button(class = "clear-completed", onclick = clearCompleted):
text "Clear completed (" & &completedCount & ")"
setOnHashChange(proc(hash: cstring) =
if hash == cstring"#/": filter = all
elif hash == cstring"#/completed": filter = completed
elif hash == cstring"#/active": filter = active
)
setRenderer createDom
proc onload(session: cstring) {.exportc.} =
for i in 0..1: # 0_000:
entries.add((cstring"Entry " & &i, false))
init()

65
experiments/menus.nim Normal file
View file

@ -0,0 +1,65 @@
## Example program that shows how to create menus with Karax.
include prelude
import jstrutils, kdom
proc contentA(): VNode =
result = buildHtml(tdiv):
text "content A"
proc contentB(): VNode =
result = buildHtml(tdiv):
text "content B"
proc contentC(): VNode =
result = buildHtml(tdiv):
text "content C"
type
MenuItemHandler = proc(): VNode
var content: MenuItemHandler = contentA
proc menuAction(x: MenuItemHandler): proc() =
result = proc() = content = x
proc buildMenu(): VNode =
result = buildHtml(tdiv):
nav(class="navbar is-primary"):
tdiv(class="navbar-brand"):
a(class="navbar-item", onclick = menuAction(contentA)):
strong:
text "My awesome menu"
tdiv(id="navMenuTransparentExample", class="navbar-menu"):
tdiv(class = "navbar-start"):
tdiv(class="navbar-item has-dropdown is-hoverable"):
a(class="navbar-link", onclick = menuAction(contentB)):
text "Masters"
tdiv(class="navbar-dropdown is-boxed"):
a(class="navbar-item", onclick = menuAction(contentC)):
text "Inventory"
a(class="navbar-item", onclick = menuAction(contentC)):
text "Product"
hr(class="navbar-divider"):
a(class="navbar-item", onclick = menuAction(contentC)):
text "Product"
tdiv(class="navbar-item has-dropdown is-hoverable"):
a(class="navbar-link", onclick = menuAction(contentC)):
text "Transactions"
tdiv(class = "navbar-dropdown is-boxed", id="blogDropdown"):
a(class="navbar-item", onclick = menuAction(contentC)):
text "Purchase Bill"
a(class="navbar-item", onclick = menuAction(contentC)):
text "Purchase Return"
a(class="navbar-item", onclick = menuAction(contentC)):
text "Sale Bill"
hr(class="navbar-divider"):
a(class="navbar-item", onclick = menuAction(contentC)):
text "Stock Adjustment"
content()
setRenderer buildMenu

View file

@ -0,0 +1,18 @@
include karax / prelude
import karax / kdom
proc myInput: VNode =
result = buildHtml:
input()
var inp = vnodeToDom(myInput())
proc myAwesomeComponent(x: Node): VNode =
result = dthunk(x)
proc main: VNode =
result = myAwesomeComponent(inp)
setRenderer main

120
experiments/nextgen.nim Normal file
View file

@ -0,0 +1,120 @@
import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, reactive
proc textInput*(text: RString; focus: RBool): VNode {.track.} =
proc onFlip(ev: Event; target: VNode) =
focus <- not focus.value
proc onKeyupEnter(ev: Event; target: VNode) =
text <- target.value
proc onkeyup(ev: Event; n: VNode) =
# keep displayValue up to date, but do not tell the client yet!
text.value = n.value
result = buildHtml(input(`type`="text",
value=text.value, onblur=onFlip, onfocus=onFlip,
onkeyupenter=onkeyupenter, onkeyup=onkeyup, setFocus=focus.value))
var
errmsg = rstr("")
makeReactive:
type
User = ref object
firstname, lastname: cstring
selected: bool
var gu = newRSeq(@[ (User(rawFirstname: "Some", rawLastname: "Body")),
(User(rawFirstname: "Some", rawLastname: "One")),
(User(rawFirstname: "Some", rawLastname: "Two"))])
var prevSelected: User = nil #newReactive[User](nil)
proc unselect() =
if prevSelected != nil:
prevSelected.selected = false
prevSelected = nil
proc select(u: User) =
unselect()
u.selected = true
prevSelected = u
proc toUI*(isFirstname: bool): RString =
result = RString()
result.subscribe proc (v: cstring) =
if v.len > 0:
let p = prevSelected #selected.value
if p != nil:
if isFirstName:
p.firstname = v
else:
p.lastname = v
unselect()
errmsg <- ""
else:
errmsg <- "name must not be empty"
var inpFirstname = toUI(true)
var inpLastname = toUI(false)
proc adaptFocus(def = false): RBool =
result = RBool()
result.value = def
when false:
result.subscribe proc (hasFocus: bool) =
if not hasFocus:
unselect()
var focusA = adaptFocus()
var focusB = adaptFocus()
proc styler(): VStyle =
result = style(
(StyleAttr.position, cstring"relative"),
(StyleAttr.paddingLeft, cstring"10px"),
(StyleAttr.paddingRight, cstring"5px"),
(StyleAttr.height, cstring"30px"),
(StyleAttr.lineHeight, cstring"30px"),
(StyleAttr.border, cstring"solid 8px " & (if focusA.value: cstring"red" else: cstring"black")),
(StyleAttr.fontSize, cstring"12px"),
(StyleAttr.fontWeight, cstring"600")
)
var clicks = 0
proc renderUser(u: User): VNode {.track.} =
result = buildHtml(tdiv):
if u.selected:
inpFirstname <- u.firstname
tdiv:
textInput inpFirstname, focusA
inpLastname <- u.lastname
tdiv:
textInput inpLastname, focusB
else:
button:
text "..."
proc onclick(ev: Event; n: VNode) =
select(u)
text u.firstname & " " & u.lastname
button:
text "(x)"
proc onclick(ev: Event; n: VNode) =
gu.deleteElem(u)
proc main(gu: RSeq[User]): VNode =
result = buildHtml(tdiv):
tdiv:
button:
text "Add User"
proc onclick(ev: Event; n: VNode) =
inc clicks
gu.add User(rawFirstname: "Added", rawLastname: &clicks)
tdiv:
text errmsg
vmapIt(gu, tdiv, renderUser(it))
proc init(): VNode = main(gu)
setInitializer(init)

251
experiments/oldwidgets.nim Normal file
View file

@ -0,0 +1,251 @@
var
linkCounter: int
proc link*(id: int): VNode =
result = newVNode(VNodeKind.anchor)
result.setAttr("href", "#")
inc linkCounter
result.setAttr("id", $linkCounter & ":" & $id)
proc link*(action: EventHandler): VNode =
result = newVNode(VNodeKind.anchor)
result.setAttr("href", "#")
result.setOnclick action
when false:
proc button*(caption: cstring; action: EventHandler; disabled=false): VNode =
result = newVNode(VNodeKind.button)
result.add text(caption)
if action != nil:
result.setOnClick action
if disabled:
result.setAttr("disabled", "true")
proc select*(choices: openarray[cstring]): VNode =
result = newVNode(VNodeKind.select)
var i = 0
for c in choices:
result.add tree(VNodeKind.option, [(cstring"value", toCstr(i))], text(c))
inc i
proc select*(choices: openarray[(int, cstring)]): VNode =
result = newVNode(VNodeKind.select)
for c in choices:
result.add tree(VNodeKind.option, [(cstring"value", toCstr(c[0]))], text(c[1]))
var radioCounter: int
proc radio*(choices: openarray[(int, cstring)]): VNode =
result = newVNode(VNodeKind.fieldset)
var i = 0
inc radioCounter
for c in choices:
let id = cstring"radio_" & c[1] & toCstr(i)
var kid = tree(VNodeKind.input, [(cstring"type", cstring"radio"),
(cstring"id", id), (cstring"name", cstring"radio" & toCStr(radioCounter)),
(cstring"value", toCStr(c[0]))])
if i == 0:
kid.setAttr(cstring"checked", cstring"checked")
var lab = tree(VNodeKind.label, [(cstring"for", id)], text(c[1]))
kid.add lab
result.add kid
inc i
proc tag*(kind: VNodeKind; id=cstring(nil), class=cstring(nil)): VNode =
result = newVNode(kind)
result.id = id
result.class = class
proc tdiv*(id=cstring(nil), class=cstring(nil)): VNode = tag(VNodeKind.tdiv, id, class)
proc span*(id=cstring(nil), class=cstring(nil)): VNode = tag(VNodeKind.span, id, class)
proc valueAsInt*(e: Node): int = parseInt(e.value)
proc th*(s: cstring): VNode =
result = newVNode(VNodeKind.th)
result.add text(s)
proc td*(s: string): VNode =
result = newVNode(VNodeKind.td)
result.add text(s)
proc td*(s: VNode): VNode =
result = newVNode(VNodeKind.td)
result.add s
proc td*(class: cstring; s: VNode): VNode =
result = newVNode(VNodeKind.td)
result.add s
result.class = class
proc table*(class=cstring(nil), kids: varargs[VNode]): VNode =
result = tag(VNodeKind.table, nil, class)
for k in kids: result.add k
proc tr*(kids: varargs[VNode]): VNode =
result = newVNode(VNodeKind.tr)
for k in kids:
if k.kind in {VNodeKind.td, VNodeKind.th}:
result.add k
else:
result.add td(k)
proc suffix*(s, prefix: cstring): cstring =
if s.startsWith(prefix):
result = s.substr(prefix.len)
else:
kout(cstring"bug! " & s & cstring" does not start with " & prefix)
proc suffixAsInt*(s, prefix: cstring): int = parseInt(suffix(s, prefix))
#proc ceil(f: float): int {.importc: "Math.ceil", nodecl.}
proc realtimeInput*(val: cstring; action: EventHandler): VNode =
var timer: Timeout
proc onkeyup(ev: Event; n: VNode) =
proc wrapper() = keyeventBody()
if timer != nil: clearTimeout(timer)
timer = setTimeout(wrapper, 400)
result = tree(VNodeKind.input, [(cstring"type", cstring"text")])
result.value = val
result.addEventListener(EventKind.onkeyup, onkeyup)
proc enterInput*(id, val: cstring; action: EventHandler): VNode =
proc onkeyup(ev: Event; n: VNode) =
if ev.keyCode == 13: keyeventBody()
result = tree(VNodeKind.input, [(cstring"type", cstring"text")])
result.id = id
result.value = val
result.addEventListener(EventKind.onkeyup, onkeyup)
proc setOnEnter*(n: VNode; action: EventHandler) =
proc onkeyup(ev: Event; n: VNode) =
if ev.keyCode == 13: keyeventBody()
n.addEventListener(EventKind.onkeyup, onkeyup)
proc setOnscroll*(action: proc(min, max: VKey; diff: int)) =
var oldY = window.pageYOffset
proc wrapper(ev: Event) =
let dir = window.pageYOffset - oldY
if dir == 0: return
var a = VKey high(int)
var b = VKey 0
var h, count: int
document.visibleKeys(a, b, h, count)
let avgh = h / count
let diff = toInt(dir.float / avgh)
if diff != 0:
oldY = window.pageYOffset
action(a, b, diff)
redraw()
document.addEventListener("scroll", wrapper)
when false:
var plugins {.exportc.}: seq[(string, proc())] = @[]
proc onInput(val: cstring) =
kout val
if val == "dyn":
kout(plugins.len)
if plugins.len > 0:
plugins[0][1]()
var
images: seq[cstring] = @[cstring"a", "b", "c", "d"]
proc carousel*(): VNode =
var currentIndex = 0
proc next(ev: Event; n: VNode) =
currentIndex = (currentIndex + 1) mod images.len
proc prev(ev: Event; n: VNode) =
currentIndex = (currentIndex - 1) mod images.len
result = buildHtml(tdiv):
text images[currentIndex]
button(onclick = next):
text "Next"
button(onclick = prev):
text "Previous"
#proc targetElem*(e: Event): Element = cast[Element](e.target)
#proc getElementsByClassName*(cls: cstring): seq[Element] {.importc:
# "document.getElementsByClassName", nodecl.}
#proc textContent(e: Node): cstring {.
# importcpp: "#.textContent", nodecl.}
proc isElementInViewport(el: Node; h: var int): bool =
let rect = el.getBoundingClientRect()
h = rect.bottom - rect.top
result = rect.top >= 0 and rect.left >= 0 and
rect.bottom <= clientHeight() and
rect.right <= clientWidth()
proc visibleKeys(e: Node; a, b: var VKey; h, count: var int) =
# we only care about nodes that have a key:
var hh = 0
# do not recurse if there is a 'key' field already:
if e.key >= 0:
if isElementInViewport(e, hh):
inc count
inc h, hh
a = min(a, e.key)
b = max(b, e.key)
else:
for i in 0..<e.len:
visibleKeys(e[i], a, b, h, count)
template toState(x): untyped = "state" & x
proc accessState(sv: string): NimNode {.compileTime.} =
newTree(nnkBracketExpr, newIdentNode(sv), newIdentNode("key"))
proc stateDecl(n: NimNode; names: TableRef[string, bool]; decl, init: NimNode) =
case n.kind
of nnkVarSection, nnkLetSection:
for c in n:
expectKind c, nnkIdentDefs
let typ = c[^2]
let val = c[^1]
let usedType = if typ.kind != nnkEmpty: typ else: val
if usedType.kind == nnkEmpty:
error("cannot determine the variable's type", c)
for i in 0 .. c.len-3:
let v = $c[i]
let sv = toState v
decl.add quote do:
var `sv` = newJDict[VKey, `usedType`]()
if val.kind != nnkEmpty:
init.add newTree(nnkAsgn, accessState(sv), val)
names[v] = true
of nnkStmtList, nnkStmtListExpr:
for x in n: stateDecl(x, names, decl, init)
of nnkDo:
stateDecl(n.body, names, decl, init)
of nnkCommentStmt: discard
else:
error("invalid 'state' declaration", n)
proc doState(n: NimNode; names: TableRef[string, bool];
decl, init: NimNode): NimNode =
result = n
case n.kind
of nnkCallKinds:
# handle 'state' declaration and remove it from the AST:
if n.len == 2 and repr(n[0]) == "state":
stateDecl(n[1], names, decl, init)
result = newTree(nnkEmpty)
of nnkSym, nnkIdent:
let v = $n
if v in names:
let sv = toState v
result = accessState(sv)
else:
for i in 0..<n.len:
result[i] = doState(n[i], names, decl, init)

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta charSet="utf-8"/>
<title>Refs</title>
</head>
<body id="body">
<div id="ROOT"></div>
<script type="text/javascript" src="demo.js"></script>
</body>
</html>

58
experiments/refs/demo.nim Normal file
View file

@ -0,0 +1,58 @@
include karaxprelude
import future, sequtils
type
CustomRef = ref object of VComponent
method onAttach(r: CustomRef) =
kout(cstring"custom ref attached")
method onDetach(r: CustomRef) =
kout(cstring"custom ref detached")
var
modelData = @[5, 2, 4]
refA = VComponent()
refB = VComponent()
refC = CustomRef()
refSeq = newSeq[VComponent]()
proc onClick(ev: Event, n: VNode) =
modelData.add(0)
proc showRefs() =
kout(refA)
kout(refB)
kout(refC)
for i in 0 ..< refSeq.len:
kout(refSeq[i].vnode)
#kout(refSeq.map(nref => nref.vnode))
proc secureRefSlot(i: int): VComponent =
while refSeq.len <= i:
refSeq.add(VComponent())
result = refSeq[i]
proc view(): VNode =
result = buildHtml():
tdiv:
button(onclick=onClick):
text "click me"
# storing refs to single elements is straightforward now
if modelData.len mod 2 == 0:
tdiv(nref=refA):
text "A"
tdiv(nref=refB):
text "B"
tdiv(nref=refC):
text "C"
# It's a bit more tricky when containers are involved:
for i, x in modelData.pairs:
tdiv(nref=secureRefSlot(i)):
text "List item: " & $x
proc renderer(): VNode =
view()
setRenderer(renderer, "ROOT", showRefs)

View file

@ -0,0 +1 @@
--path: "../../src"

View file

@ -10,7 +10,7 @@
<div id="ROOT" />
<script type="text/javascript" src="nimcache/scrollapp.js"></script>
<script type="text/javascript" src="scrollapp.js"></script>
<script type="text/javascript">
onload(window.location.pathname.split('/')[1])
</script>

View file

@ -1,17 +1,14 @@
import vdom, karax, karaxdsl, jdict, jstrutils
import vdom, karax, karaxdsl, jdict, jstrutils, compact
type
Filter = enum
all, active, completed
const
WindowSize = 50 # overestimating it does not harm
var
entries: seq[(cstring, bool)]
visibleA = 0
visibleB = 10_000
visibleB = 10
selectedEntry = -1
filter: Filter
@ -52,34 +49,15 @@ proc toChecked(checked: bool): cstring =
proc selected(v: Filter): cstring =
(if filter == v: cstring"selected" else: cstring(nil))
when false:
proc entry(id: int, d: cstring; completed, selected: bool): proc (): VNode =
var lastid: type(id)
var lastd: type(d)
var lastCompleted: type(completed)
var lastSelected: type(selected)
proc inner(): VNode =
if lastid != id or lastd != d or lastCompleted != completed or lastSelected != selected:
result = logic(id, d, completed, selected)
lastid = id
lastd = d
lastCompleted = completed
lastSelected = selected
result = inner
proc lazyEntry(args: seq[VNode]): VNode =
let id = args[0].intValue
let d = args[1].text
let completed = args[2].intValue != 0
let selected = args[3].intValue != 0
proc createEntry(id: int; d: cstring;
completed: bool, selected: bool): VNode {.component.} =
result = buildHtml(tr):
li(class=toClass(completed), key = id):
if not selected:
tdiv(class = "view"):
input(class = "toggle", `type` = "checkbox", checked = toChecked(completed),
onclick=toggleEntry, key = id)
label(onDblClick=editHandler, key = id):
label(onclick=editHandler, key = id):
text d
button(class = "destroy", key = id, onclick = removeHandler)
else:
@ -87,9 +65,6 @@ proc lazyEntry(args: seq[VNode]): VNode =
onfocusLost = focusLost,
onenter = editEntry, value = d, setFocus)
proc createEntry(id: int; d: cstring; completed, selected: bool): VNode =
result = thunk(lazyEntry, id, d, completed, selected)
proc myscroll(ev: Event; n: VNode) =
kout cstring"scrolling event here"
@ -105,10 +80,6 @@ proc createDom(): VNode =
input(class = "toggle-all", `type` = "checkbox", name = "toggle")
label(`for` = "toggle-all", onclick = onAllDone):
text "Mark all as complete"
tdiv(onscroll = myscroll, style = "overflow: visible; width: 0px; display: block; max-height: 1em"):
for i in 0..4:
tdiv:
text entries[i][0]
var entriesCount = 0
var completedCount = 0

View file

@ -0,0 +1,25 @@
## Example that shows how to accomplish an "infinitely scrolling" app.
include karax / prelude
import karax / [kdom, vstyles]
var entries: seq[cstring] = @[]
for i in 1..500:
entries.add(cstring("Entry ") & &i)
proc scrollEvent(ev: Event; n: VNode) =
let d = n.dom
if d != nil and inViewport(d.lastChild):
# "load" more data:
for i in 1..50:
entries.add(cstring("Loaded Entry ") & &i)
proc createDom(): VNode =
result = buildHtml():
tdiv(onscroll=scrollEvent, style=style(
(StyleAttr.height, cstring"400px"), (StyleAttr.overflow, cstring"scroll"))):
for x in entries:
tdiv:
text x
setRenderer createDom

378
experiments/style.css Normal file
View file

@ -0,0 +1,378 @@
html,
body {
margin: 0;
padding: 0;
}
.todomvc-wrapper {
visibility: visible !important;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #4d4d4d;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
font-weight: 300;
}
button,
input[type="checkbox"] {
outline: none;
}
.hidden {
display: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp h1 {
position: absolute;
top: -155px;
width: 100%;
font-size: 100px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
border: 0;
outline: none;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
}
.new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
label[for='toggle-all'] {
display: none;
}
.toggle-all {
position: absolute;
top: -55px;
left: -12px;
width: 60px;
height: 34px;
text-align: center;
border: none; /* Mobile Safari */
}
.toggle-all:before {
content: '>';
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
.toggle-all:checked:before {
color: #737373;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle:after {
content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
}
.todo-list li .toggle:checked:after {
content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
}
.todo-list li label {
white-space: pre-line;
word-break: break-all;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
transition: color 0.4s;
}
.todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover {
color: #af5b5e;
}
.todo-list li .destroy:after {
content: 'x';
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
color: #777;
padding: 10px 15px;
height: 20px;
text-align: center;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li a.selected,
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
position: relative;
}
.clear-completed:hover {
text-decoration: underline;
}
.info {
margin: 65px auto 0;
color: #bfbfbf;
/* font-size: 10px; */
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.info p {
line-height: 1;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
.toggle-all {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}

View file

@ -0,0 +1,752 @@
## Raw DOM manipulation. No DOM diff'ing, no cry.
import macros, tables
from strutils import startsWith, toLowerAscii, toUpperAscii
when defined(js):
type kstring* = cstring
else:
type kstring* = string
type
Tag* {.pure.} = enum
text = "#text",
html, head, title, base, link, meta, style,
script, noscript,
body, section, nav, article, aside,
h1, h2, h3, h4, h5, h6,
header, footer, address, main
p, hr, pre, blockquote, ol, ul, li,
dl, dt, dd,
figure, figcaption,
tdiv = "div",
a, em, strong, small,
strikethrough = "s", cite, quote,
dfn, abbr, data, time, code, `var` = "var", samp,
kdb, sub, sup, italic = "i", bold = "b", underlined = "u",
mark, ruby, rt, rp, bdi, dbo, span, br, wbr,
ins, del, img, iframe, embed, `object` = "object",
param, video, audio, source, track, canvas, map,
area, svg, math,
table, caption, colgroup, col, tbody, thead,
tfoot, tr, td, th,
form, fieldset, legend, label, input, button,
select, datalist, optgroup, option, textarea,
keygen, output, progress, meter,
details, summary, command, menu
type
EventKind* {.pure.} = enum ## The events supported by the virtual DOM.
onclick, ## An element is clicked.
oncontextmenu, ## An element is right-clicked.
ondblclick, ## An element is double clicked.
onkeyup, ## A key was released.
onkeydown, ## A key is pressed.
onkeypressed, # A key was pressed.
onfocus, ## An element got the focus.
onblur, ## An element lost the focus.
onchange, ## The selected value of an element was changed.
onscroll, ## The user scrolled within an element.
onmousedown, ## A pointing device button (usually a mouse) is pressed
## on an element.
onmouseenter, ## A pointing device is moved onto the element that
## has the listener attached.
onmouseleave, ## A pointing device is moved off the element that
## has the listener attached.
onmousemove, ## A pointing device is moved over an element.
onmouseout, ## A pointing device is moved off the element that
## has the listener attached or off one of its children.
onmouseover, ## A pointing device is moved onto the element that has
## the listener attached or onto one of its children.
onmouseup, ## A pointing device button is released over an element.
ondrag, ## An element or text selection is being dragged (every 350ms).
ondragend, ## A drag operation is being ended (by releasing a mouse button
## or hitting the escape key).
ondragenter, ## A dragged element or text selection enters a valid drop target.
ondragleave, ## A dragged element or text selection leaves a valid drop target.
ondragover, ## An element or text selection is being dragged over a valid
## drop target (every 350ms).
ondragstart, ## The user starts dragging an element or text selection.
ondrop, ## An element is dropped on a valid drop target.
onsubmit, ## A form is submitted
oninput, ## An input value changes
onkeyupenter, ## vdom extension: an input field received the ENTER key press
onkeyuplater ## vdom extension: a key was pressed and some time
## passed (useful for on-the-fly text completions)
macro buildLookupTables(): untyped =
var a = newTree(nnkBracket)
for i in low(Tag)..high(Tag):
let x = $i
let y = if x[0] == '#': x else: toUpperAscii(x)
a.add(newCall("kstring", newLit(y)))
var e = newTree(nnkBracket)
for i in low(EventKind)..high(EventKind):
e.add(newCall("kstring", newLit(substr($i, 2))))
template tmpl(a, e) {.dirty.} =
const
toTagName*: array[Tag, kstring] = a
toEventName*: array[EventKind, kstring] = e
result = getAst tmpl(a, e)
buildLookupTables()
# ---------------- CSS ----------------------------------------------
type
StyleAttr* {.pure.} = enum
## The style attributes supported by the virtual DOM.
## Reference: https://www.w3schools.com/jsref/dom_obj_style.asp
alignContent
alignItems
alignSelf
animation
animationDelay
animationDirection
animationDuration
animationFillMode
animationIterationCount
animationName
animationTimingFunction
animationPlayState
background
backgroundAttachment
backgroundColor
backgroundImage
backgroundPosition
backgroundRepeat
backgroundClip
backgroundOrigin
backgroundSize
backfaceVisibility
border
borderBottom
borderBottomColor
borderBottomLeftRadius
borderBottomRightRadius
borderBottomStyle
borderBottomWidth
borderCollapse
borderColor
borderImage
borderImageOutset
borderImageRepeat
borderImageSlice
borderImageSource
borderImageWidth
borderLeft
borderLeftColor
borderLeftStyle
borderLeftWidth
borderRadius
borderRight
borderRightColor
borderRightStyle
borderRightWidth
borderSpacing
borderStyle
borderTop
borderTopColor
borderTopLeftRadius
borderTopRightRadius
borderTopStyle
borderTopWidth
borderWidth
bottom
boxDecorationBreak
boxShadow
boxSizing
captionSide
clear
clip
color
columnCount
columnFill
columnGap
columnRule
columnRuleColor
columnRuleStyle
columnRuleWidth
columns
columnSpan
columnWidth
content
counterIncrement
counterReset
cursor
direction
display
emptyCells
filter
flex
flexBasis
flexDirection
flexFlow
flexGrow
flexShrink
flexWrap
cssFloat
font
fontFamily
fontSize
fontSizeAdjust
fontStretch
fontStyle
fontVariant
fontWeight
hangingPunctuation
height
hyphens
icon
imageOrientation
justifyContent
left
letterSpacing
lineHeight
listStyle
listStyleImage
listStylePosition
listStyleType
margin
marginBottom
marginLeft
marginRight
marginTop
maxHeight
maxWidth
minHeight
minWidth
navDown
navIndex
navLeft
navRight
navUp
opacity
order
orphans
outline
outlineColor
outlineOffset
outlineStyle
outlineWidth
overflow
overflowX
overflowY
padding
paddingBottom
paddingLeft
paddingRight
paddingTop
pageBreakAfter
pageBreakBefore
pageBreakInside
perspective
perspectiveOrigin
pointerEvents
position
quotes
resize
right
scrollbar3dLightColor # note: missing in w3schools reference
scrollbarArrowColor # note: missing in w3schools reference
scrollbarBaseColor # note: missing in w3schools reference
scrollbarDarkshadowColor # note: missing in w3schools reference
scrollbarFaceColor # note: missing in w3schools reference
scrollbarHighlightColor # note: missing in w3schools reference
scrollbarShadowColor # note: missing in w3schools reference
scrollbarTrackColor # note: missing in w3schools reference
tableLayout
tabSize
textAlign
textAlignLast
textDecoration
textDecorationColor
textDecorationLine
textDecorationStyle
textIndent
textJustify
textOverflow
textShadow
textTransform
top
transform
transformOrigin
transformStyle
transition
transitionDelay
transitionDuration
transitionProperty
transitionTimingFunction
unicodeBidi
userSelect
verticalAlign
visibility
whiteSpace
width
wordBreak
wordSpacing
wordWrap
widows
zIndex
macro buildStyleLookupTable(): untyped =
var e = newTree(nnkBracket)
for i in low(StyleAttr)..high(StyleAttr):
e.add(newCall("kstring", newLit($i)))
template tmpl(e) {.dirty.} =
const
toStyleAttrName: array[StyleAttr, kstring] = e
result = getAst tmpl(e)
buildStyleLookupTable()
type
StyleObj {.importc.} = object
background*: cstring
backgroundAttachment*: cstring
backgroundColor*: cstring
backgroundImage*: cstring
backgroundPosition*: cstring
backgroundRepeat*: cstring
border*: cstring
borderBottom*: cstring
borderBottomColor*: cstring
borderBottomStyle*: cstring
borderBottomWidth*: cstring
borderColor*: cstring
borderLeft*: cstring
borderLeftColor*: cstring
borderLeftStyle*: cstring
borderLeftWidth*: cstring
borderRight*: cstring
borderRightColor*: cstring
borderRightStyle*: cstring
borderRightWidth*: cstring
borderStyle*: cstring
borderTop*: cstring
borderTopColor*: cstring
borderTopStyle*: cstring
borderTopWidth*: cstring
borderWidth*: cstring
bottom*: cstring
captionSide*: cstring
clear*: cstring
clip*: cstring
color*: cstring
cursor*: cstring
direction*: cstring
display*: cstring
emptyCells*: cstring
cssFloat*: cstring
font*: cstring
fontFamily*: cstring
fontSize*: cstring
fontStretch*: cstring
fontStyle*: cstring
fontVariant*: cstring
fontWeight*: cstring
height*: cstring
left*: cstring
letterSpacing*: cstring
lineHeight*: cstring
listStyle*: cstring
listStyleImage*: cstring
listStylePosition*: cstring
listStyleType*: cstring
margin*: cstring
marginBottom*: cstring
marginLeft*: cstring
marginRight*: cstring
marginTop*: cstring
maxHeight*: cstring
maxWidth*: cstring
minHeight*: cstring
minWidth*: cstring
overflow*: cstring
padding*: cstring
paddingBottom*: cstring
paddingLeft*: cstring
paddingRight*: cstring
paddingTop*: cstring
pageBreakAfter*: cstring
pageBreakBefore*: cstring
pointerEvents*: cstring
position*: cstring
right*: cstring
scrollbar3dLightColor*: cstring
scrollbarArrowColor*: cstring
scrollbarBaseColor*: cstring
scrollbarDarkshadowColor*: cstring
scrollbarFaceColor*: cstring
scrollbarHighlightColor*: cstring
scrollbarShadowColor*: cstring
scrollbarTrackColor*: cstring
tableLayout*: cstring
textAlign*: cstring
textDecoration*: cstring
textIndent*: cstring
textTransform*: cstring
transform*: cstring
top*: cstring
verticalAlign*: cstring
visibility*: cstring
width*: cstring
wordSpacing*: cstring
zIndex*: int
Style* = ref StyleObj
ElementObj {.importc.} = object
id*, class*, value*: cstring
disabled*: bool
style*: Style
Element* = ref ElementObj
DocumentObj {.importc.} = object
Document = ref DocumentObj
proc setStyle(s: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.}
proc applyStyles*(e: Element; pairs: openArray[(StyleAttr, kstring)]) =
for x in pairs:
e.style.setStyle(toStyleAttrName[x[0]], x[1])
# ------- Tree manipulation ---------------------------
var document {.importc.}: Document
proc parent*(x: Element): Element {.importcpp: "#.parentNode".}
proc up*(x: Element; className: cstring): Element =
result = x
while result != nil and result.class != className:
result = result.parent
proc len*(x: Element): int {.importcpp: "#.childNodes.length".}
proc `[]`*(x: Element; idx: int): Element {.importcpp: "#.childNodes[#]".}
proc getElementById*(id: cstring): Element {.importc: "document.getElementById", nodecl.}
proc add*(n, child: Element) {.importcpp: "appendChild".}
proc removeChild(n, child: Element) {.importcpp.}
proc replaceChild(n, newNode, oldNode: Element) {.importcpp.}
proc replace*(self, by: Element) = replaceChild(self.parent, by, self)
proc delete*(self: Element) = removeChild(self.parent, self)
proc insertBefore(n, newNode, before: Element) {.importcpp.}
proc insert*(before, newNode: Element) = insertBefore(before.parent, newNode, before)
proc createTextNode(d: Document, text: cstring): Element {.importcpp.}
proc text*(s: kstring): Element = createTextNode(document, s)
iterator items*(n: Element): Element =
for i in 0..<n.len: yield n[i]
proc createElement(d: Document, identifier: cstring): Element {.importcpp.}
proc setAttr*(n: Element; key, val: cstring) {.importcpp: "#.setAttribute(@)".}
proc tree*(kind: Tag; kids: varargs[Element]): Element =
result = createElement(document, toTagName[kind])
for k in kids: result.add k
proc tree*(kind: Tag; attrs: openarray[(kstring, kstring)];
kids: varargs[Element]): Element =
result = tree(kind, kids)
for a in attrs: result.setAttr(a[0], a[1])
# other arbitrary stuff belonging to Element
proc focus*(e: Element) {.importcpp.}
proc scrollIntoView*(e: Element) {.importcpp.}
proc blur*(e: Element) {.importcpp.}
proc toChecked*(checked: bool): cstring =
(if checked: cstring"checked" else: cstring(nil))
proc toDisabled*(disabled: bool): cstring =
(if disabled: cstring"disabled" else: cstring(nil))
# -------------- Timer handling --------------------
type
Timeout* {.importc.} = ref object of RootObj
proc setTimeout*(action: proc(); ms: int): Timeout {.importc, nodecl.}
proc clearTimeout*(t: Timeout) {.importc, nodecl.}
# -------------- Event handling --------------------
type
EventObj {.importc.} = object
target*: Element
altKey*, ctrlKey*, metaKey*, shiftKey*: bool
code*: cstring
isComposing*: bool
key*: cstring
keyCode*: int
location*: int
Event* = ref EventObj
EventHandler* = proc (ev: Event) {.closure.}
proc addEventListener(e: Element, ev: cstring, cb: proc(ev: Event),
useCapture = false) {.importcpp.}
proc addEventHandler*(e: Element; k: EventKind; action: EventHandler) =
case k
of EventKind.onkeyuplater:
proc laterWrapper(): EventHandler =
let action = action
var timer: Timeout
result = proc (ev: Event) =
proc wrapper() = action(ev)
if timer != nil: clearTimeout(timer)
timer = setTimeout(wrapper, 400)
e.addEventListener("keyup", laterWrapper())
of EventKind.onkeyupenter:
proc enterWrapper(): EventHandler =
let action = action
result = proc (ev: Event) =
if ev.keyCode == 13: action(ev)
e.addEventListener("keyup", enterWrapper())
else:
e.addEventListener(toEventName[k], action)
proc addEventHandler*(e: Element; k: EventKind; action: proc()) =
addEventHandler e, k, (proc (ev: Event) = action())
proc prepareDragData*(ev: Event; datatype, data: cstring)
{.importcpp: "#.dataTransfer.setData(@)".}
proc recvDragData*(ev: Event; datatype: cstring): cstring
{.importcpp: "#.dataTransfer.getData(@)".}
proc preventDefault*(ev: Event) {.importcpp.}
# ------------------ Init handling -----------------------------------
proc replaceById*(newTree: Element; id: cstring = "ROOT") =
let x = getElementById(id)
x.parent.replaceChild(newTree, x)
newTree.id = id
proc setWindowOnload(h: EventHandler) {.importcpp: "window.onload = #".}
proc setInitializer*(initializer: proc (hashPart: kstring): Element;
root: cstring = "ROOT") =
var onhashChange {.importc: "window.onhashchange".}: proc()
var hashPart {.importc: "window.location.hash".}: cstring
setWindowOnload proc (ev: Event) =
replaceById root, initializer(hashPart)
onhashchange = proc () =
replaceById root, initializer(hashPart)
# ------------------ DSL section -------------------------------------
const
StmtContext = ["inc", "echo", "dec", "!"]
SpecialAttrs = ["id", "class", "value"]
type
ComponentKind {.pure.} = enum
None,
Tag
var
allcomponents {.compileTime.} = initTable[string, ComponentKind]()
proc isComponent(x: string): ComponentKind {.compileTime.} =
allcomponents.getOrDefault(x)
proc addTags() {.compileTime.} =
let x = (bindSym"Tag").getTypeImpl
expectKind(x, nnkEnumTy)
for i in ord(Tag.html)..ord(Tag.high):
# +1 because of empty node at the start of the enum AST:
let tag = $x[i+1]
allcomponents[tag] = ComponentKind.Tag
static:
addTags()
proc getName(n: NimNode): string =
case n.kind
of nnkIdent:
result = $n.ident
of nnkAccQuoted:
result = ""
for i in 0..<n.len:
result.add getName(n[i])
of nnkStrLit..nnkTripleStrLit:
result = n.strVal
else:
#echo repr n
expectKind(n, nnkIdent)
proc newDotAsgn(tmp: NimNode, key: string, x: NimNode): NimNode =
result = newTree(nnkAsgn, newDotExpr(tmp, newIdentNode key), x)
proc toKstring(n: NimNode): NimNode =
if n.kind == nnkStrLit:
result = newCall(bindSym"kstring", n)
else:
result = copyNimNode(n)
for child in n:
result.add toKstring(child)
proc tcall2(n, tmpContext: NimNode): NimNode =
# we need to distinguish statement and expression contexts:
# every call statement 's' needs to be transformed to 'dest.add s'.
# If expressions need to be distinguished from if statements. Since
# we know we start in a statement context, it's pretty simple to
# figure out expression contexts: In calls everything is an expression
# (except for the last child of the macros we consider here),
# lets, consts, types can be considered as expressions
# case is complex, calls are assumed to produce a value.
when defined(js):
template evHandler(): untyped = bindSym"addEventHandler"
else:
template evHandler(): untyped = ident"addEventHandler"
case n.kind
of nnkLiterals, nnkIdent, nnkSym, nnkDotExpr, nnkBracketExpr:
if tmpContext != nil:
result = newCall(bindSym"add", tmpContext, n)
else:
result = n
of nnkForStmt, nnkIfExpr, nnkElifExpr, nnkElseExpr,
nnkOfBranch, nnkElifBranch, nnkExceptBranch, nnkElse,
nnkConstDef, nnkWhileStmt, nnkIdentDefs, nnkVarTuple:
# recurse for the last son:
result = copyNimTree(n)
let L = n.len
assert n.len == result.len
if L > 0:
result[L-1] = tcall2(result[L-1], tmpContext)
of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkTryStmt,
nnkFinally:
# recurse for every child:
result = copyNimNode(n)
for x in n:
result.add tcall2(x, tmpContext)
of nnkCaseStmt:
# recurse for children, but don't add call for case ident
result = copyNimNode(n)
result.add n[0]
for i in 1 ..< n.len:
result.add tcall2(n[i], tmpContext)
of nnkProcDef:
let name = getName n[0]
if name.startsWith"on":
# turn it into an anon proc:
let anon = copyNimTree(n)
anon[0] = newEmptyNode()
if tmpContext == nil:
error "no Element to attach the event handler to"
else:
result = newCall(evHandler(), tmpContext,
newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi"))
else:
result = n
of nnkVarSection, nnkLetSection, nnkConstSection:
result = n
of nnkCallKinds - {nnkInfix}:
let op = getName(n[0])
if isComponent(op) == ComponentKind.Tag:
let tmp = genSym(nskLet, "tmp")
let call = newCall(bindSym"tree", newDotExpr(bindSym"Tag", n[0]))
result = newTree(
if tmpContext == nil: nnkStmtListExpr else: nnkStmtList,
newLetStmt(tmp, call))
for i in 1 ..< n.len:
# named parameters are transformed into attributes or events:
let x = n[i]
if x.kind == nnkExprEqExpr:
let key = getName x[0]
if key.startsWith("on"):
result.add newCall(evHandler(),
tmp, newDotExpr(bindSym"EventKind", x[0]), x[1])
elif key in SpecialAttrs:
result.add newDotAsgn(tmp, key, x[1])
elif eqIdent(key, "style"):
result.add newCall(bindSym"applyStyles", tmp, toKstring x[1])
elif eqIdent(key, "setFocus"):
result.add newCall(bindSym"focus", tmp)
else:
result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
elif eqIdent(x, "setFocus"):
result.add newCall(bindSym"focus", tmp)
else:
result.add tcall2(x, tmp)
if tmpContext == nil:
result.add tmp
else:
result.add newCall(bindSym"add", tmpContext, tmp)
elif tmpContext != nil and op notin StmtContext:
var hasEventHandlers = false
for i in 1..<n.len:
let it = n[i]
if it.kind in {nnkProcDef, nnkStmtList}:
hasEventHandlers = true
break
if not hasEventHandlers:
result = newCall(bindSym"add", tmpContext, n)
else:
let tmp = genSym(nskLet, "tmp")
var slicedCall = newCall(n[0])
let ex = newTree(nnkStmtListExpr)
ex.add newEmptyNode() # will become the let statement
for i in 1..<n.len:
let it = n[i]
if it.kind in {nnkProcDef, nnkStmtList}:
ex.add tcall2(it, tmp)
else:
slicedCall.add it
ex[0] = newLetStmt(tmp, slicedCall)
ex.add tmp
result = newCall(bindSym"add", tmpContext, ex)
elif op == "!" and n.len == 2:
result = n[1]
else:
result = n
else:
result = n
macro buildHtml*(tag, children: untyped): Element =
let kids = newProc(procType=nnkDo, body=children)
expectKind kids, nnkDo
var call: NimNode
if tag.kind in nnkCallKinds:
call = tag
else:
call = newCall(tag)
call.add body(kids)
result = tcall2(call, nil)
when defined(debugKaraxDsl):
echo repr result
macro buildHtml*(children: untyped): Element =
let kids = newProc(procType=nnkDo, body=children)
expectKind kids, nnkDo
result = tcall2(body(kids), nil)
when defined(debugKaraxDsl):
echo repr result
macro flatHtml*(tag: untyped): Element =
result = tcall2(tag, nil)
when defined(debugKaraxDsl):
echo repr result

View file

@ -0,0 +1,151 @@
import knete, widgets
import karax / jstrutils
type
Attachable* = ref object of RootObj ## an element that is attachable to
## DOM elements.
attachedTo*: seq[Element]
TaskId = int
Task = ref object
id: TaskId
name, desc: kstring
ColumnId = distinct int
Column = ref object of Attachable
id: ColumnId
header: kstring
tasks: seq[Task]
Board = ref object
columns: seq[Column]
var b = Board(columns: @[Column(id: ColumnId 0, header: "To Do"),
Column(id: ColumnId 1, header: "Doing"),
Column(id: ColumnId 2, header: "Done")])
proc removeTask(t: TaskId) =
for c in b.columns:
for i in 0 ..< c.tasks.len:
if c.tasks[i].id == t:
delete(c.tasks, i)
break
proc moveTask(t: TaskId; dest: Column) =
for c in b.columns:
for i in 0 ..< c.tasks.len:
let task = c.tasks[i]
if task.id == t:
delete(c.tasks, i)
dest.tasks.add task
return
var nextTaskId = 0 ## negative because they are not yet backed
## up by the database
proc createTask(c: Column; name, desc: kstring): Task =
dec nextTaskId
result = Task(id: TaskId nextTaskId, name: name, desc: desc)
c.tasks.add result
# --------------------- UI -----------------------------------------------
proc renderTask(t: Task): Element =
proc dodelete =
removeTask t.id
delete result
proc dragstart(ev: Event) =
ev.prepareDragData("taskid", ev.target.id)
result = buildHtml():
tdiv(draggable="true", ondragstart=dragstart, id = &t.id):
bindField t.name
br()
bindField t.desc
button(onclick = dodelete):
text cross
type
NewTaskPanel = object
c: Column
e: Element
proc open(p: var NewTaskPanel; c: Column) =
p.e.style.display = "block"
p.c = c
proc newTaskDialog(): NewTaskPanel =
var nameInp = buildHtml():
input(setFocus = true)
var descInp = buildHtml():
input()
proc close =
result.e.style.display = "none"
proc submit =
let t = createTask(result.c, nameInp.value, descInp.value)
close()
result.c.attachedTo[0].add renderTask(t)
result.e = buildHtml():
tdiv(style={display: "none", position: "fixed",
left: "0", top: "0", width: "100%", height: "100%",
overflow: "auto",
backgroundColor: "rgb(0,0,0)",
backgroundColor: "rgba(0,0,0,0.4)", zIndex: "1"}):
tdiv(style={backgroundColor: "#fefefe",
margin: "15% auto", padding: "20px", border: "1px solid #888",
width: "80%"}):
span(onclick = close, style={color: "#aaa",
cssFloat: "right",
fontSize: "28px",
fontWeight: "bold"}):
text cross
p:
text "Task name"
nameInp
p:
text "Task description"
descInp
span(onclick = submit, style={color: "#0f0",
cssFloat: "left",
fontWeight: "bold"}):
text "Submit"
var dialog = newTaskDialog()
proc renderColumn(c: Column): Element =
proc doadd(c: Column): proc () =
result = proc() = dialog.open(c)
proc allowDrop(ev: Event) = ev.preventDefault()
proc drop(ev: Event) =
ev.preventDefault()
let data = ev.recvDragData("taskid")
moveTask(parseInt data, c)
ev.target.up("mycolumn").add(getElementById(data))
result = buildHtml():
tdiv(class = "mycolumn", style = {cssFloat: "left", width: "20%"},
ondrop = drop, ondragover = allowDrop):
tdiv(class = "myheader"):
bindField c.header
span(onclick = doadd(c)):
text plus
for t in c.tasks:
renderTask(t)
c.attachedTo.add result
proc renderBoard(b: Board): Element =
result = buildHtml(tdiv):
dialog.e
for c in b.columns:
renderColumn(c)
proc main(hashPart: kstring): Element =
result = renderBoard(b)
setInitializer main

View file

@ -0,0 +1,43 @@
## Common widget implemenations
import knete
const
cross* = "x"
plus* = kstring"+"
const
LEFT* = 37
UP* = 38
RIGHT* = 39
DOWN* = 40
TAB* = 9
ESC* = 27
ENTER* = 13
proc editable*(x: kstring; onchanged: proc (value: kstring);
isEdited = false): Element =
proc onenter(ev: Event) = ev.target.blur()
proc submit(ev: Event) =
onchanged(ev.target.value)
# close the loop: This is a common pattern in Knete.
replace(result, editable(ev.target.value, onchanged, false))
proc makeEditable() =
# close the loop: This is a common pattern in Knete.
replace(result, editable(x, onchanged, true))
result = buildHtml():
if isEdited:
input(class = "edit",
onblur = submit,
onkeyupenter = onenter,
value = x,
setFocus = true)
else:
span(onclick = makeEditable):
text x
template bindField*(field): Element =
editable field, proc (value: kstring) =
field = value

View file

@ -1,12 +1,16 @@
# Package
version = "0.1.0"
author = "Araq"
version = "1.2.1"
author = "Andreas Rumpf"
description = "Karax is a framework for developing single page applications in Nim."
license = "MIT"
# Dependencies
requires "nim >= 0.16.1"
requires "nim >= 0.18.0"
requires "ws"
requires "dotenv"
skipDirs = @["examples", "experiments", "tests"]
skipDirs = @["examples/todoapp"]
bin = @["karax/tools/karun"]
installExt = @["nim"]

160
karax/autocomplete.nim Normal file
View file

@ -0,0 +1,160 @@
import karax, karaxdsl, vdom, kdom, jstrutils
type
AutocompleteState* = ref object
choices, candidates: seq[cstring]
selected, maxMatches: int
showCandidates, controlPressed: bool
proc newAutocomplete*(choices: seq[cstring]; maxMatches = 5): AutocompleteState =
## Creates a new state for the autocomplete widget. ``maxMatches`` is the maximum
## number of elements to show.
AutocompleteState(choices: choices, candidates: @[],
selected: -1, maxMatches: maxMatches, showCandidates: false,
controlPressed: false)
proc autocomplete*(s: AutocompleteState; onselection: proc(s: cstring)): VNode =
var inp: VNode
proc commit(ev: Event) =
s.showCandidates = false
onselection(inp.dom.value)
when false:
if inp.dom != nil:
echo "setting to A ", inp.dom.value.isNil
result.text = inp.dom.value
else:
echo "setting to B ", inp.text.isNil
result.text = inp.text
for e in result.events:
if e[0] == EventKind.onchange:
e[1](ev, result)
proc onkeyuplater(ev: Event; n: VNode) =
if not s.controlPressed:
let v = n.value
s.candidates.setLen 0
for c in s.choices:
if v.len == 0 or c.containsIgnoreCase(v): s.candidates.add(c)
proc onkeydown(ev: Event; n: VNode) =
const
LEFT = 37
UP = 38
RIGHT = 39
DOWN = 40
TAB = 9
ESC = 27
ENTER = 13
# UP: Move focus to the previous item. If on first item, move focus to the input.
# If on the input, move focus to last item.
# DOWN: Move focus to the next item. If on last item, move focus to the input.
# If on the input, move focus to the first item.
# ESCAPE: Close the menu.
# ENTER: Select the currently focused item and close the menu.
# TAB: Select the currently focused item, close the menu, and
# move focus to the next focusable element
s.controlPressed = false
case ev.keyCode
of UP:
s.controlPressed = true
s.showCandidates = true
if s.selected > 0:
dec s.selected
n.setInputText s.candidates[s.selected]
of DOWN:
s.controlPressed = true
s.showCandidates = true
if s.selected < s.candidates.len - 1:
inc s.selected
n.setInputText s.candidates[s.selected]
of ESC:
s.showCandidates = false
s.controlPressed = true
of ENTER:
s.controlPressed = true
# inp.setInputText s.choices[i]
commit(ev)
else:
discard
proc window(s: AutocompleteState): (int, int) =
var first, last: int
if s.selected >= 0:
first = s.selected - (s.maxMatches div 2)
last = s.selected + (s.maxMatches div 2)
if first < 0: first = 0
# too few because we had to trim first?
if (last - first + 1) < s.maxMatches: last = first + s.maxMatches - 1
else:
first = 0
last = s.maxMatches - 1
if last > high(s.candidates): last = high(s.candidates)
# still too few because we're at the end?
if (last - first + 1) < s.maxMatches:
first = last - s.maxMatches + 1
if first < 0: first = 0
result = (first, last)
inp = buildHtml:
input(onkeyuplater = onkeyuplater,
onkeydown = onkeydown,
# onblur = proc (ev: Event; n: VNode) = commit(ev),
onfocus = proc (ev: Event; n: VNode) =
onkeyuplater(ev, n)
s.showCandidates = true)
proc select(i: int): proc(ev: Event; n: VNode) =
result = proc(ev: Event; n: VNode) =
s.selected = i
s.showCandidates = false
inp.setInputText s.choices[i]
commit(ev)
result = buildHtml(table):
tr:
td:
inp
if s.showCandidates:
let (first, last) = window(s)
for i in first..last:
tr:
td(onclick = select(i), class = cstring"button " &
(if i == s.selected: cstring"is-primary" else: "")):
text s.candidates[i]
when isMainModule:
const suggestions = @[cstring"ActionScript",
"AppleScript",
"Asp",
"BASIC",
"C",
"C++",
"Clojure",
"COBOL",
"Erlang",
"Fortran",
"Groovy",
"Haskell",
"Java",
"JavaScript",
"Lisp",
"Nim",
"Perl",
"PHP",
"Python",
"Ruby",
"Scala",
"Scheme"]
var s = newAutocomplete(suggestions)
proc main(): VNode =
result = buildHtml(tdiv):
autocomplete(s, proc (s: cstring) = echo "now ", s)#:
# proc onchange(ev: Event; n: VNode) =
# echo "now selected ", n.kind
setRenderer(main)

114
karax/compact.nim Normal file
View file

@ -0,0 +1,114 @@
## Components in Karax are built by the ``.component`` macro annotation.
when defined(js):
import jdict, kdom
import macros, vdom, tables, strutils, kbase
when defined(js):
var
vcomponents* = newJDict[cstring, proc(args: seq[VNode]): VNode]()
else:
var
vcomponents* = newTable[kstring, proc(args: seq[VNode]): VNode]()
type
ComponentKind* {.pure.} = enum
None,
Tag,
VNode
var
allcomponents {.compileTime.} = initTable[string, ComponentKind]()
proc isComponent*(x: string): ComponentKind {.compileTime.} =
allcomponents.getOrDefault(x)
proc addTags() {.compileTime.} =
let x = (bindSym"VNodeKind").getTypeImpl
expectKind(x, nnkEnumTy)
for i in ord(VNodeKind.html)..ord(VNodeKind.high):
# +1 because of empty node at the start of the enum AST:
let tag = $x[i+1]
allcomponents[tag] = ComponentKind.Tag
static:
addTags()
proc unpack(symbolicType: NimNode; index: int): NimNode {.compileTime.} =
#let t = symbolicType.getTypeImpl
let t = repr(symbolicType)
case t
of "cstring":
result = quote do:
args[`index`].text
of "int", "VKey":
result = quote do:
args[`index`].intValue
of "bool":
result = quote do:
args[`index`].intValue != 0
elif t.endsWith"Kind":
result = quote do:
`symbolicType`(args[`index`].intValue)
else:
# just pass it along, maybe there is some conversion for it:
result = quote do:
args[`index`]
proc newname*(n: NimNode): NimNode =
if n.kind == nnkPostfix:
n[1] = newname(n[1])
result = n
elif n.kind == nnkSym:
result = ident(n.strVal)
else:
result = n
when defined(js):
macro compact*(prc: untyped): untyped =
## A 'compact' tree generation proc is one that only depends on its
## inputs and should be stored as a compact virtual DOM tree and
## only expanded on demand (when its inputs changed).
var n = prc.copyNimNode
for i in 0..6: n.add prc[i].copyNimTree
expectKind(n, nnkProcDef)
if n[0].kind == nnkEmpty:
error("please pass a non anonymous proc", n[0])
let name = n[0]
let params = params(n)
let rettype = repr params[0]
var isvirtual = ComponentKind.None
if rettype == "VNode":
isvirtual = ComponentKind.VNode
else:
error "component must return VNode", params[0]
let realName = if name.kind == nnkPostfix: name[1] else: name
let nn = $realName
n[0] = ident("inner" & nn)
var unpackCall = newCall(n[0])
var counter = 0
for i in 1 ..< params.len:
let param = params[i]
let L = param.len
let typ = param[L-2]
for j in 0 .. L-3:
unpackCall.add unpack(typ, counter)
inc counter
template vwrapper(pname, unpackCall) {.dirty.} =
proc pname(args: seq[VNode]): VNode =
unpackCall
template vregister(key, val) =
bind jdict.`[]=`
`[]=`(vcomponents, kstring(key), val)
result = newTree(nnkStmtList, n)
if isvirtual == ComponentKind.VNode:
result.add getAst(vwrapper(newname name, unpackCall))
result.add getAst(vregister(newLit(nn), realName))
allcomponents[nn] = isvirtual
when defined(debugKaraxDsl):
echo repr result

32
karax/errors.nim Normal file
View file

@ -0,0 +1,32 @@
## Error handling logic for form input validation.
import karax, jdict
var
gerrorMsgs = newJDict[cstring, cstring]()
gerrorCounter = 0
proc hasErrors*(): bool = gerrorCounter != 0
proc hasError*(field: cstring): bool = gerrorMsgs.contains(field) and len(gerrorMsgs[field]) > 0
proc disableOnError*(): cstring = toDisabled(hasErrors())
proc getError*(field: cstring): cstring =
if not gerrorMsgs.contains(field):
result = ""
else:
result = gerrorMsgs[field]
proc setError*(field, msg: cstring) =
let previous = getError(field)
if len(msg) == 0:
if len(previous) != 0: dec(gerrorCounter)
else:
if len(previous) == 0: inc(gerrorCounter)
gerrorMsgs[field] = msg
proc clearErrors*() =
let m = gerrorMsgs
for k in m.keys:
setError(k, "")

152
karax/i18n.nim Normal file
View file

@ -0,0 +1,152 @@
## Karax -- Single page applications for Nim.
## i18n support for Karax applications. We distinguish between:
##
## - translate: Lookup text translations
## - localize: Localize Date and Time objects to local formats
##
## Localization has not yet been implemented.
import languages, jdict, jstrutils
var currentLanguage = Language.enUS
proc getLanguage(): cstring =
{.emit: "`result` = (navigator.languages && navigator.languages.length) ? navigator.languages[0] : navigator.language;".}
proc detectLanguage*(): Language =
let x = getLanguage()
for i in low(Language)..high(Language):
if languageToCode[i] == x: return i
return Language.enUS
proc setCurrentLanguage*(newLanguage = detectLanguage()) =
currentLanguage = newLanguage
proc getCurrentLanguage*(): Language = currentLanguage
type Translation* = JDict[cstring, cstring]
var
translations: array[Language, Translation]
proc registerTranslation*(lang: Language; t: Translation) = translations[lang] = t
proc addTranslation*(lang: Language; key, val: cstring) =
if translations[lang].isNil: translations[lang] = newJDict[cstring, cstring]()
translations[lang][key] = val
proc translate(x: cstring): cstring =
let y = translations[currentLanguage]
if y != nil and y.contains(x):
result = y[x]
else:
result = x
type TranslatedString* = distinct cstring
template i18n*(x: string): TranslatedString = TranslatedString(translate(cstring x))
proc raiseInvalidFormat(errmsg: string) =
raise newException(ValueError, errmsg)
discard """
$[1:item|items]1$ selected.
"""
proc `{}`(x: cstring; i: int): char {.importcpp: "#.charCodeAt(#)".}
proc sadd(s: JSeq[char]; c: char) {.importcpp: "#.push(String.fromCharCode(#))".}
proc parseChoice(f: cstring; i, choice: int,
r: JSeq[char]) =
var i = i
while i < f.len:
var n = 0
let oldI = i
var toAdd = false
while i < f.len and f{i} >= '0' and f{i} <= '9':
n = n * 10 + ord(f{i}) - ord('0')
inc i
if oldI != i:
if f{i} == ':':
inc i
else:
raiseInvalidFormat"':' after number expected"
toAdd = choice == n
else:
# an else section does not start with a number:
toAdd = true
while i < f.len and f{i} != ']' and f{i} != '|':
if toAdd: r.sadd f{i}
inc i
if toAdd: break
inc i
proc join(x: JSeq[char]): cstring {.importcpp: "#.join(\"\")".}
proc add(x: JSeq[char]; y: cstring) =
for i in 0..<y.len: x.sadd y{i}
proc `%`*(formatString: TranslatedString; args: openArray[cstring]): cstring =
let f = cstring(formatString)
var i = 0
var num = 0
var r = newJSeq[char]()
while i < f.len:
if f{i} == '$' and i+1 < f.len:
inc i
case f{i}
of '#':
r.add args[num]
inc i
inc num
of '1'..'9', '-':
var j = 0
var negative = f{i} == '-'
if negative: inc i
while f{i} >= '0' and f{i} <= '9':
j = j * 10 + ord(f{i}) - ord('0')
inc i
let idx = if not negative: j-1 else: args.len-j
r.add args[idx]
of '$':
inc(i)
r.add '$'
of '[':
let start = i+1
while i < f.len and f{i} != ']': inc i
inc i
if i >= f.len: raiseInvalidFormat"']' expected"
case f{i}
of '#':
parseChoice(f, start, parseInt args[num], r)
inc i
inc num
of '1'..'9', '-':
var j = 0
var negative = f{i} == '-'
if negative: inc i
while f{i} >= '0' and f{i} <= '9':
j = j * 10 + ord(f{i}) - ord('0')
inc i
let idx = if not negative: j-1 else: args.len-j
parseChoice(f, start, parseInt args[idx], r)
else: raiseInvalidFormat"argument index expected after ']'"
else:
raiseInvalidFormat("'#', '$', or number expected")
if i < f.len and f{i} == '$': inc i
else:
r.sadd f{i}
inc i
result = join(r)
when isMainModule:
addTranslation(Language.deDE, "$#$ -> $#$", "$2 macht $1")
setCurrentLanguage(Language.deDE)
echo(i18n"$#$ -> $#$" % [cstring"1", "2"])
echo(i18n"$[1:item |items ]1$ -> $1" % [cstring"1", "2"])
echo(i18n"$[1:item |items ]1 -> $1" % [cstring"0", "2"])
echo(i18n"$[1:item |items ]1 -> $1" % [cstring"2", "2"])
echo(i18n"$[1:item |items ]1$ -> $1" % [cstring"3", "2"])
echo(i18n"$1 $[1:item |4:huha |items ]1$ -> $1" % [cstring"4", "2"])

30
karax/jdict.nim Normal file
View file

@ -0,0 +1,30 @@
type
JDict*[K, V] = ref object
proc `[]`*[K, V](d: JDict[K, V], k: K): V {.importcpp: "#[#]".}
proc `[]=`*[K, V](d: JDict[K, V], k: K, v: V) {.importcpp: "#[#] = #".}
proc newJDict*[K, V](): JDict[K, V] {.importcpp: "{@}".}
proc contains*[K, V](d: JDict[K, V], k: K): bool {.importcpp: "#.hasOwnProperty(#)".}
proc del*[K, V](d: JDict[K, V], k: K) {.importcpp: "delete #[#]".}
iterator keys*[K, V](d: JDict[K, V]): K =
var kkk: K
{.emit: ["for (", kkk, " in ", d, ") {"].}
yield kkk
{.emit: ["}"].}
type
JSeq*[T] = ref object
proc `[]`*[T](s: JSeq[T], i: int): T {.importcpp: "#[#]", noSideEffect.}
proc `[]=`*[T](s: JSeq[T], i: int, v: T) {.importcpp: "#[#] = #", noSideEffect.}
proc newJSeq*[T](len: int = 0): JSeq[T] {.importcpp: "new Array(#)".}
proc len*[T](s: JSeq[T]): int {.importcpp: "#.length", noSideEffect.}
proc add*[T](s: JSeq[T]; x: T) {.importcpp: "#.push(#)", noSideEffect.}
proc shrink*[T](s: JSeq[T]; shorterLen: int) {.importcpp: "#.length = #", noSideEffect.}

67
karax/jjson.nim Normal file
View file

@ -0,0 +1,67 @@
## This module implements some small zero-overhead 'JsonNode' type
## and helper that maps directly to JavaScript objects.
type
JsonNode* {.importc.} = ref object
proc `[]`*(obj: JsonNode; fieldname: cstring): JsonNode {.importcpp: "#[#]".}
proc `[]`*(obj: JsonNode; index: int): JsonNode {.importcpp: "#[#]".}
proc `[]=`*[T](obj: JsonNode; fieldname: cstring; value: T)
{.importcpp: "#[#] = #".}
proc length(x: JsonNode): int {.importcpp: "#.length".}
proc len*(x: JsonNode): int = (if x.isNil: 0 else: x.length)
proc newJsonNode*(fields: varargs[(cstring, JsonNode)]): JsonNode =
result = JsonNode()
for f in fields:
result[f[0]] = f[1]
proc newJObject*(): JsonNode =
result = JsonNode()
proc newJArray*(elements: varargs[JsonNode]): JsonNode {.importcpp: "#".}
proc newJNull*(): JsonNode = nil
template `%`*(x: typed): JsonNode = cast[JsonNode](x)
template `%`*(x: string): JsonNode = cast[JsonNode](cstring x)
proc getNum*(x: JsonNode): int {.importcpp: "#".}
proc getInt*(x: JsonNode): int {.importcpp: "#".}
proc getStr*(x: JsonNode): cstring {.importcpp: "#".}
proc getFNum*(x: JsonNode): cstring {.importcpp: "#".}
iterator items*(x: JsonNode): JsonNode =
for i in 0..<len(x): yield x[i]
import macros
proc toJson(x: NimNode): NimNode {.compiletime.} =
case x.kind
of nnkBracket:
result = newCall(bindSym"newJArray")
for i in 0 ..< x.len:
result.add(toJson(x[i]))
of nnkTableConstr:
result = newCall(bindSym"newJsonNode")
for i in 0 ..< x.len:
x[i].expectKind nnkExprColonExpr
let key = x[i][0]
let a = if x[i].kind in {nnkIdent, nnkSym, nnkAccQuoted}:
newLit($key)
else:
key
result.add newTree(nnkPar, newCall(bindSym"cstring", a), toJson(x[i][1]))
of nnkCurly:
x.expectLen(0)
result = newCall(bindSym"newJObject")
of nnkNilLit:
result = newCall(bindSym"newJNull")
else:
result = newCall(bindSym"%", x)
macro `%*`*(x: untyped): untyped =
## Convert an expression to a JsonNode directly, without having to specify
## `%` for every element.
result = toJson(x)

View file

@ -4,8 +4,14 @@ proc split*(s, sep: cstring): seq[cstring] {.importcpp, nodecl.}
proc split*(s, sep: cstring; max: int): seq[cstring] {.importcpp, nodecl.}
proc strip*(s: cstring): cstring {.importcpp: "#.trim()", nodecl.}
proc startsWith*(a, b: cstring): bool {.importcpp: "startsWith", nodecl.}
proc contains*(a, b: cstring): bool {.importcpp: "(#.indexOf(#)>=0)", nodecl.}
proc containsIgnoreCase*(a, b: cstring): bool {.
importcpp: """(#.search(new RegExp(#.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$$\|]/g, "\\$$&") , "i"))>=0)""", nodecl.}
proc substr*(s: cstring; start: int): cstring {.importcpp: "substr", nodecl.}
proc substr*(s: cstring; start, length: int): cstring {.importcpp: "substr", nodecl.}
@ -14,10 +20,14 @@ proc `&`*(a, b: cstring): cstring {.importcpp: "(# + #)", nodecl.}
proc toCstr*(s: int): cstring {.importcpp: "((#)+'')", nodecl.}
proc `&`*(s: int): cstring {.importcpp: "((#)+'')", nodecl.}
proc `&`*(s: bool): cstring {.importcpp: "((#)+'')", nodecl.}
proc `&`*(s: float): cstring {.importcpp: "((#)+'')", nodecl.}
proc `&`*(s: cstring): cstring {.importcpp: "(#)", nodecl.}
proc isInt*(s: cstring): bool {.asmNoStackFrame.} =
asm """
return s.match(/^[0-9]+$/);
return `s`.match(/^[0-9]+$/);
"""
proc parseInt*(s: cstring): int {.importcpp: "parseInt(#, 10)", nodecl.}
proc parseFloat*(s: cstring): BiggestFloat {.importc, nodecl.}

17
karax/jwebsockets.nim Normal file
View file

@ -0,0 +1,17 @@
## Websockets support for the JSON backend.
type
MessageEvent* {.importc.} = ref object
data*: cstring
WebSocket* {.importc.} = ref object
onmessage*: proc (e: MessageEvent)
onopen*: proc (e: MessageEvent)
proc newWebSocket*(url, key: cstring): WebSocket
{.importcpp: "new WebSocket(@)".}
proc newWebSocket*(url: cstring): WebSocket
{.importcpp: "new WebSocket(@)".}
proc send*(w: WebSocket; data: cstring) {.importcpp.}

132
karax/kajax.nim Normal file
View file

@ -0,0 +1,132 @@
## Karax -- Single page applications for Nim.
## This module implements support for `ajax`:idx: socket
## handling.
import karax
import jsffi except `&`
import jscore
import dom
type
ProgressEvent* {.importc.}= object
loaded*: float
total*: float
FormData* {.importc.} = JsObject
HttpRequest* {.importc.} = ref object
ThisObj {.importc.} = ref object
readyState, status: int
responseText, statusText: cstring
XMLHttpRequestUpload* {.importc.} = JsObject
proc newFormData*(): FormData {.importcpp: "new FormData()".}
proc append*(f: FormData, key: cstring, value: Blob) {.importcpp:"#.append(@)".}
proc append*(f: FormData, key: cstring, value: cstring) {.importcpp:"#.append(@)".}
proc setRequestHeader*(r: HttpRequest; a, b: cstring) {.importcpp: "#.setRequestHeader(@)".}
proc statechange*(r: HttpRequest; cb: proc()) {.importcpp: "#.onreadystatechange = #".}
proc send*(r: HttpRequest; data: cstring) {.importcpp: "#.send(#)".}
proc send*(r: HttpRequest, data: Blob) {.importcpp: "#.send(#)".}
proc open*(r: HttpRequest; meth, url: cstring; async: bool) {.importcpp: "#.open(@)".}
proc newRequest*(): HttpRequest {.importcpp: "new XMLHttpRequest(@)".}
when not declared(dom.File):
type
DomFile = ref FileObj
FileObj {.importc.} = object of Blob
lastModified: int
name: cstring
else:
type
DomFile = dom.File
proc uploadFile*(url: cstring, file: Blob, onprogress :proc(data: ProgressEvent),
cont: proc (httpStatus: int; response: cstring);
headers: openarray[(cstring, cstring)] = []) =
proc contWrapper(httpStatus: int; response: cstring) =
cont(httpStatus, response)
proc upload(r: HttpRequest):XMLHttpRequestUpload {.importcpp: "#.upload".}
var this {.importc: "this".}: ThisObj
var formData = newFormData()
formData.append("upload_file",file)
formData.append("filename", DomFile(file).name)
let ajax = newRequest()
ajax.open("POST", url, true)
for a, b in items(headers):
ajax.setRequestHeader(a, b)
ajax.statechange proc() =
if this.readyState == 4:
contWrapper(this.status, this.responseText)
ajax.upload.onprogress = onprogress
ajax.send(formData.to(cstring))
proc ajax*(meth, url: cstring; headers: openarray[(cstring, cstring)];
data: cstring;
cont: proc (httpStatus: int; response: cstring);
doRedraw: bool = true,
kxi: KaraxInstance = kxi,
useBinary: bool = false,
blob: Blob = nil) =
proc contWrapper(httpStatus: int; response: cstring) =
cont(httpStatus, response)
if doRedraw: redraw(kxi)
var this {.importc: "this".}: ThisObj
let ajax = newRequest()
ajax.open(meth, url, true)
for a, b in items(headers):
ajax.setRequestHeader(a, b)
ajax.statechange proc() =
if this.readyState == 4:
if this.status == 200:
contWrapper(this.status, this.responseText)
else:
contWrapper(this.status, this.responseText)
if useBinary:
ajax.send(blob)
else:
ajax.send(data)
proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)];
data: cstring;
cont: proc (httpStatus: int, response: cstring);
doRedraw: bool = true,
kxi: KaraxInstance = kxi) =
ajax("POST", url, headers, data, cont, doRedraw, kxi)
proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)];
data: Blob;
cont: proc (httpStatus: int, response: cstring);
doRedraw: bool = true,
kxi: KaraxInstance = kxi) =
ajax("POST", url, headers, "", cont, doRedraw, kxi, true, data)
proc ajaxGet*(url: cstring; headers: openarray[(cstring, cstring)];
cont: proc (httpStatus: int, response: cstring);
doRedraw: bool = true,
kxi: KaraxInstance = kxi) =
ajax("GET", url, headers, nil, cont, doRedraw, kxi)
proc ajaxPut*(url: cstring; headers: openarray[(cstring, cstring)];
data: cstring;
cont: proc (httpStatus: int, response: cstring);
doRedraw: bool = true,
kxi: KaraxInstance = kxi) =
ajax("PUT", url, headers, data, cont, doRedraw, kxi)
proc ajaxDelete*(url: cstring; headers: openarray[(cstring, cstring)];
cont: proc (httpStatus: int, response: cstring);
doRedraw: bool = true,
kxi: KaraxInstance = kxi) =
ajax("DELETE", url, headers, nil, cont, doRedraw, kxi)
proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".}
proc fromJson*[T](blob: cstring): T {.importc: "JSON.parse".}

834
karax/karax.nim Normal file
View file

@ -0,0 +1,834 @@
## Karax -- Single page applications for Nim.
import kdom, vdom, jstrutils, compact, jdict, vstyles
export kdom.Event, kdom.Blob
when defined(nimNoNil):
{.experimental: "notnil".}
proc kout*[T](x: T) {.importc: "console.log", varargs.}
## The preferred way of debugging karax applications.
type
PatchKind = enum
pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach, pkSame
Patch = object
k: PatchKind
parent, current: Node
newNode, oldNode: VNode
PatchV = object
parent, newChild: VNode
pos: int
ComponentPair = object
oldNode, newNode: VComponent
parent, current: Node
type
RouterData* = ref object ## information that is passed to the 'renderer' callback
hashPart*: cstring ## the hash part of the URL for routing.
KaraxInstance* = ref object ## underlying karax instance. Usually you don't have
## know about this.
rootId: cstring
renderer: proc (data: RouterData): VNode {.closure.}
currentTree: VNode
postRenderCallback: proc (data: RouterData)
toFocus: Node
toFocusV: VNode
renderId: int
rendering: bool
patches: seq[Patch] # we reuse this to save allocations
patchLen: int
patchesV: seq[PatchV]
patchLenV: int
runCount: int
components: seq[ComponentPair]
surpressRedraws*: bool
byId: JDict[cstring, VNode]
when defined(stats):
recursion: int
orphans: JDict[cstring, bool]
var
kxi*: KaraxInstance ## The current Karax instance. This is always used
## as the default. **Note**: Within the karax DSL
## always a symbol of the name *kxi* is assumed, so
## if you have a local karax instance to use instead
## in your 'buildHtml' statement, it needs to be named
## 'kxi'.
proc setFocus*(n: VNode; enabled = true; kxi: KaraxInstance = kxi) =
if enabled:
kxi.toFocusV = n
# ----------------- event wrapping ---------------------------------------
template nativeValue(ev): cstring = cast[Element](ev.target).value
template setNativeValue(ev, val) = cast[Element](ev.target).value = val
template keyeventBody() =
let v = nativeValue(ev)
n.value = v
assert action != nil
action(ev, n)
if n.value != v:
setNativeValue(ev, n.value)
# Do not call redraw() here! That is already done
# by ``karax.addEventHandler``.
proc karaxEvents(d: Node): JSeq[(cstring, NativeEventHandler)] {.importcpp: "#.karaxEvents".}
proc `karaxEvents=`(d: Node; value: JSeq[(cstring, NativeEventHandler)]) {.importcpp: "#.karaxEvents = #".}
proc addEventShell(d: Node; name: cstring; h: NativeEventHandler) =
# The DOM is such a pathetic piece of junk that it doesn't
# offer 'removeAllEventHandlers()'. Hence we store the event
# handler twice in 'd' so that we can emulate this properly.
# This is required to fix bug #139.
d.addEventListener(name, h)
if d.karaxEvents == nil:
d.karaxEvents = newJSeq[(cstring, NativeEventHandler)]()
d.karaxEvents.add((name, h))
proc removeAllEventHandlers(d: Node) =
if d.karaxEvents != nil:
for i in 0..<d.karaxEvents.len:
d.removeEventListener(d.karaxEvents[i][0], d.karaxEvents[i][1])
proc wrapEvent(d: Node; n: VNode; k: EventKind;
action: EventHandler): NativeEventHandler =
proc stdWrapper(): NativeEventHandler =
let action = action
let n = n
result = proc (ev: Event) =
if n.kind == VNodeKind.textarea or n.kind == VNodeKind.input or n.kind == VNodeKind.select:
keyeventBody()
else: action(ev, n)
proc enterWrapper(): NativeEventHandler =
let action = action
let n = n
result = proc (ev: Event) =
if cast[KeyboardEvent](ev).keyCode == 13: keyeventBody()
proc laterWrapper(): NativeEventHandler =
let action = action
let n = n
var timer: Timeout
result = proc (ev: Event) =
proc wrapper() = keyeventBody()
if timer != nil: clearTimeout(timer)
timer = setTimeout(wrapper, 400)
case k
of EventKind.onkeyuplater:
result = laterWrapper()
d.addEventShell("keyup", result)
of EventKind.onkeyupenter:
result = enterWrapper()
d.addEventShell("keyup", result)
else:
result = stdWrapper()
d.addEventShell(toEventName[k], result)
# --------------------- DOM diff -----------------------------------------
template detach(n: VNode) =
addPatch(kxi, pkDetach, nil, nil, nil, n)
template attach(n: VNode) =
n.dom = result
if n.id != nil: kxi.byId[n.id] = n
proc applyEvents(n: VNode) =
let dest = n.dom
for i in 0..<len(n.events):
n.events[i][2] = wrapEvent(dest, n, n.events[i][0], n.events[i][1])
proc getVNodeById*(id: cstring; kxi: KaraxInstance = kxi): VNode =
## Get the VNode that was marked with ``id``. Returns ``nil``
## if no node exists.
if kxi.byId.contains(id):
result = kxi.byId[id]
proc toDom*(n: VNode; useAttachedNode: bool; kxi: KaraxInstance = nil): Node =
if useAttachedNode:
if n.dom != nil:
if n.id != nil: kxi.byId[n.id] = n
return n.dom
if n.kind == VNodeKind.text:
result = document.createTextNode(n.text)
attach n
elif n.kind == VNodeKind.verbatim:
result = document.createElement("div")
result.innerHTML = n.text
attach n
return result
elif n.kind == VNodeKind.vthunk:
let x = callThunk(vcomponents[n.text], n)
result = toDom(x, useAttachedNode, kxi)
#n.key = result.key
attach n
return result
elif n.kind == VNodeKind.dthunk:
result = n.dom #callThunk(dcomponents[n.text], n)
assert result != nil
#n.key = result.key
attach n
return result
elif n.kind == VNodeKind.component:
let x = VComponent(n)
if x.onAttachImpl != nil: x.onAttachImpl(x)
assert x.renderImpl != nil
if x.expanded == nil:
x.expanded = x.renderImpl(x)
# x.updatedImpl(x, nil)
assert x.expanded != nil
result = toDom(x.expanded, useAttachedNode, kxi)
attach n
return result
else:
result = document.createElement(toTag[n.kind])
attach n
for k in n:
appendChild(result, toDom(k, useAttachedNode, kxi))
# text is mapped to 'value':
if n.text != nil:
result.value = n.text
if n.id != nil:
result.id = n.id
if n.class != nil:
result.class = n.class
#if n.key >= 0:
# result.key = n.key
for k, v in attrs(n):
if v != nil:
result.setAttr(k, v)
applyEvents(n)
if kxi != nil and n == kxi.toFocusV and kxi.toFocus.isNil:
kxi.toFocus = result
if not n.style.isNil: applyStyle(result, n.style)
proc same(n: VNode, e: Node; nesting = 0): bool =
if kxi.orphans.contains(n.id): return true
if n.kind == VNodeKind.component:
result = same(VComponent(n).expanded, e, nesting+1)
elif n.kind == VNodeKind.verbatim:
result = true
elif n.kind == VNodeKind.vthunk or n.kind == VNodeKind.dthunk:
# we don't check these:
result = true
elif toTag[n.kind] == e.nodename:
result = true
if n.kind != VNodeKind.text:
# BUGFIX: Microsoft's Edge gives the textarea a child containing the text node!
if e.len != n.len and n.kind != VNodeKind.textarea:
echo "expected ", n.len, " real ", e.len, " ", toTag[n.kind], " nesting ", nesting
return false
for i in 0 ..< n.len:
if not same(n[i], e[i], nesting+1): return false
else:
echo "VDOM: ", toTag[n.kind], " DOM: ", e.nodename
proc replaceById(id: cstring; newTree: Node) =
let x = document.getElementById(id)
x.parentNode.replaceChild(newTree, x)
newTree.id = id
type
EqResult = enum
componentsIdentical, different, similar, identical, usenewNode
when defined(profileKarax):
type
DifferEnum = enum
deKind, deId, deIndex, deText, deComponent, deClass,
deSimilar
var
reasons: array[DifferEnum, int]
proc echa(a: array[DifferEnum, int]) =
for i in low(DifferEnum)..high(DifferEnum):
echo i, " value: ", a[i]
proc eq(a, b: VNode; recursive: bool): EqResult =
if a.kind != b.kind:
when defined(profileKarax): inc reasons[deKind]
return different
if a.id != b.id:
when defined(profileKarax): inc reasons[deId]
return different
result = identical
if a.index != b.index:
when defined(profileKarax): inc reasons[deIndex]
return different
if a.kind == VNodeKind.text:
if a.text != b.text:
when defined(profileKarax): inc reasons[deText]
return different # similar
elif a.kind == VNodeKind.vthunk:
if a.text != b.text: return different
if a.len != b.len: return different
for i in 0..<a.len:
if eq(a[i], b[i], recursive) == different: return different
elif a.kind == VNodeKind.dthunk:
if a.dom == b.dom:
return identical
else: # fix #119
return different
elif a.kind == VNodeKind.verbatim:
if a.text != b.text:
return different
elif b.kind == VNodeKind.component:
# different component names mean different components:
if a.text != b.text:
when defined(profileKarax): inc reasons[deComponent]
return different
#if VComponent(a).key.isNil and VComponent(b).key.isNil:
# when defined(profileKarax): inc reasons[deComponent]
# return different
if VComponent(a).key != VComponent(b).key:
when defined(profileKarax): inc reasons[deComponent]
return different
return componentsIdentical
#if:
# when defined(profileKarax): inc reasons[deClass]
# return different
if a.class != b.class or not eq(a.style, b.style) or not sameAttrs(a, b):
when defined(profileKarax): inc reasons[deSimilar]
return similar
if recursive:
if a.len != b.len:
return different
for i in 0..<a.len:
if eq(a[i], b[i], true) != identical:
return different
# Do not test event listeners here!
return result
proc updateStyles(newNode, oldNode: VNode) =
# we keep the oldNode, but take over the style from the new node:
if oldNode.dom != nil:
if newNode.style != nil: applyStyle(oldNode.dom, newNode.style)
else: oldNode.dom.style = Style()
oldNode.dom.class = newNode.class
oldNode.style = newNode.style
oldNode.class = newNode.class
proc updateAttributes(newNode, oldNode: VNode) =
# we keep the oldNode, but take over the attributes from the new node:
if oldNode.dom != nil:
for k, _ in attrs(oldNode):
oldNode.dom.removeAttribute(k)
for k, v in attrs(newNode):
if v != nil:
oldNode.dom.setAttr(k, v)
takeOverAttr(newNode, oldNode)
proc mergeEvents(newNode, oldNode: VNode; kxi: KaraxInstance) =
let d = oldNode.dom
if d != nil:
removeAllEventHandlers(d)
when false:
for i in 0..<oldNode.events.len:
let k = oldNode.events[i][0]
let name = case k
of EventKind.onkeyuplater, EventKind.onkeyupenter: cstring"keyup"
else: toEventName[k]
d.removeEventListener(name, oldNode.events[i][2])
shallowCopy(oldNode.events, newNode.events)
applyEvents(oldNode)
when false:
proc printV(n: VNode; depth: cstring = "") =
kout depth, cstring($n.kind), cstring"key ", n.index
#for k, v in pairs(n.style):
# kout depth, "style: ", k, v
if n.kind == VNodeKind.component:
let nn = VComponent(n)
if nn.expanded != nil: printV(nn.expanded, ">>" & depth)
elif n.kind == VNodeKind.text:
kout depth, n.text
for i in 0 ..< n.len:
printV(n[i], depth & " ")
proc addPatch(kxi: KaraxInstance; ka: PatchKind; parenta, currenta: Node;
na, oldNode: VNode) =
let L = kxi.patchLen
if L >= kxi.patches.len:
# allocate more space:
kxi.patches.add(Patch(k: ka, parent: parenta, current: currenta,
newNode: na, oldNode: oldNode))
else:
kxi.patches[L].k = ka
kxi.patches[L].parent = parenta
kxi.patches[L].current = currenta
kxi.patches[L].newNode = na
kxi.patches[L].oldNode = oldNode
inc kxi.patchLen
proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) =
let L = kxi.patchLenV
if L >= kxi.patchesV.len:
# allocate more space:
kxi.patchesV.add(PatchV(parent: parent, newChild: newChild, pos: pos))
else:
kxi.patchesV[L].parent = parent
kxi.patchesV[L].newChild = newChild
kxi.patchesV[L].pos = pos
inc kxi.patchLenV
proc moveDom(dest, src: VNode) =
dest.dom = src.dom
src.dom = nil
if dest.id != nil:
kxi.byId[dest.id] = dest
assert dest.len == src.len
for i in 0..<dest.len:
moveDom(dest[i], src[i])
proc applyPatch(kxi: KaraxInstance) =
for i in 0..<kxi.patchLen:
let p = kxi.patches[i]
case p.k
of pkReplace:
let nn = toDom(p.newNode, useAttachedNode = true, kxi)
if p.parent == nil:
replaceById(kxi.rootId, nn)
else:
if p.current.parentNode == p.parent:
p.parent.replaceChild(nn, p.current)
else: # fix #121
p.parent.appendChild(nn)
of pkSame:
moveDom(p.newNode, p.oldNode)
of pkRemove:
p.parent.removeChild(p.current)
of pkAppend:
let nn = toDom(p.newNode, useAttachedNode = true, kxi)
p.parent.appendChild(nn)
of pkInsertBefore:
let nn = toDom(p.newNode, useAttachedNode = true, kxi)
p.parent.insertBefore(nn, p.current)
of pkDetach:
let n = p.oldNode
if n.id != nil: kxi.byId.del(n.id)
if n.kind == VNodeKind.component:
let x = VComponent(n)
if x.onDetachImpl != nil: x.onDetachImpl(x)
# XXX for some reason this causes assertion errors otherwise:
if not kxi.surpressRedraws: n.dom = nil
kxi.patchLen = 0
for i in 0..<kxi.patchLenV:
let p = kxi.patchesV[i]
p.parent[p.pos] = p.newChild
assert p.newChild.dom != nil
kxi.patchLenV = 0
# ASSUME: We patch both the virtual DOM and the real DOM and throw away
# the newly produced DOM. Thus on updates like 'newNode.dom = oldNode.dom'
# are required. The only exception is when the top level node is replaced.
# Then we have to take the new virtual DOM. In fact, we trigger a full DOM
# rebuild then. However, we don't have to consider old event handlers then
# so everything stays simple.
# We also do not produce "Patch sets" anymore, everything is done as simply
# as possible. Ok, let's assume that we seek to update event handler lists:
# The new node has captures to itself or to other new nodes, never to old
# nodes! --> We cannot ever use the old VDOM, we have to use the new virtual
# DOM. For identical nodes we need to take over the .dom field from the old
# node since we don't recompute them. This must be done recursively. In
# vnodeToDom we have to check whether the 'dom' field was already set. If so,
# There is nothing to do.
# "Similar" nodes can have the opposite effect; consider
#
# AAAABAAAA
# AAAACDAAAA
#
# In this example B did change to C and 'D' is new. However, replacing B by
# CD is fine.
#
proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance) =
when defined(stats):
if kxi.recursion > 100:
echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode, false)
if oldNode.kind == VNodeKind.text:
echo oldNode.text
inc kxi.recursion
let result = eq(newNode, oldNode, false)
case result
of componentsIdentical:
kxi.components.add ComponentPair(oldNode: VComponent(oldNode),
newNode: VComponent(newNode),
parent: parent,
current: current)
of identical, similar:
newNode.dom = oldNode.dom
if result == similar:
updateStyles(newNode, oldNode)
updateAttributes(newNode, oldNode)
if oldNode.kind == VNodeKind.text:
oldNode.text = newNode.text
oldNode.dom.nodeValue = newNode.text
if newNode.events.len != 0 or oldNode.events.len != 0:
mergeEvents(newNode, oldNode, kxi)
let newLength = newNode.len
let oldLength = oldNode.len
if newLength == 0 and oldLength == 0: return
let minLength = min(newLength, oldLength)
assert oldNode.kind == newNode.kind
var commonPrefix = 0
# compute common prefix:
while commonPrefix < minLength:
if eq(newNode[commonPrefix], oldNode[commonPrefix], true) == identical:
kxi.addPatch(pkSame, nil, nil, newNode[commonPrefix], oldNode[commonPrefix])
inc commonPrefix
else:
break
# compute common suffix:
var oldPos = oldLength - 1
var newPos = newLength - 1
while oldPos >= commonPrefix and newPos >= commonPrefix:
if eq(newNode[newPos], oldNode[oldPos], true) == identical:
kxi.addPatch(pkSame, nil, nil, newNode[newPos], oldNode[oldPos])
dec oldPos
dec newPos
else:
break
let pos = min(oldPos, newPos) + 1
# now the different children are in commonPrefix .. pos - 1:
for i in commonPrefix..pos-1:
diff(newNode[i], oldNode[i], current, current.childNodes[i], kxi)
if oldPos + 1 == oldLength:
for i in pos..newPos:
kxi.addPatch(pkAppend, current, nil, newNode[i], nil)
else:
let before = current.childNodes[oldPos + 1]
for i in pos..newPos:
kxi.addPatch(pkInsertBefore, current, before, newNode[i], nil)
# XXX call 'attach' here?
for i in pos..oldPos:
detach(oldNode[i])
#doAssert i < current.childNodes.len
kxi.addPatch(pkRemove, current, current.childNodes[i], nil, nil)
of different:
detach(oldNode)
kxi.addPatch(pkReplace, parent, current, newNode, nil)
of usenewNode: doAssert(false, "eq returned usenewNode")
when defined(stats):
dec kxi.recursion
proc applyComponents(kxi: KaraxInstance) =
# the first 'diff' pass detects components in the VDOM. The
# 'applyComponents' expands components and so on until no
# components are left to check.
var i = 0
# beware: 'diff' appends to kxi.components!
# So this is actually a fixpoint iteration:
while i < kxi.components.len:
let x = kxi.components[i].oldNode
let newNode = kxi.components[i].newNode
when defined(karaxDebug):
echo "Processing component ", newNode.text, " changed impl set ", x.changedImpl != nil
if x.changedImpl != nil and x.changedImpl(x, newNode):
when defined(karaxDebug):
echo "Component ", newNode.text, " did change"
let current = kxi.components[i].current
let parent = kxi.components[i].parent
x.updatedImpl(x, newNode)
let oldExpanded = x.expanded
x.expanded = x.renderImpl(x)
when defined(karaxDebug):
echo "Component ", newNode.text, " re-rendered"
x.renderedVersion = x.version
if oldExpanded == nil:
detach(x)
kxi.addPatch(pkReplace, parent, current, x.expanded, nil)
when defined(karaxDebug):
echo "Component ", newNode.text, ": old expansion didn't exist"
else:
diff(x.expanded, oldExpanded, parent, current, kxi)
when false:
if res == usenewNode:
when defined(karaxDebug):
echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)"
discard "diff created a patchset for us, so this is fine"
elif res != different:
when defined(karaxDebug):
echo "Component ", newNode.text, ": re-render triggered no DOM change whatsoever"
x.expanded = oldExpanded
assert oldExpanded.dom != nil, "old expanded.dom is nil"
else:
when defined(karaxDebug):
echo "Component ", newNode.text, ": re-render triggered a DOM change (case B)"
assert x.expanded.dom != nil, "expanded.dom is nil"
inc i
setLen(kxi.components, 0)
when defined(stats):
proc depth(n: VNode; total: var int): int =
var m = 0
for i in 0..<n.len:
m = max(m, depth(n[i], total))
result = m + 1
inc total
proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) =
detach(parent[position])
let current = parent.dom
kxi.addPatch(pkRemove, current, current.childNodes[position], nil, nil)
parent.delete(position)
applyPatch(kxi)
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) =
let current = parent.dom
if position >= parent.len:
kxi.addPatch(pkAppend, current, nil, kid, nil)
parent.add(kid)
else:
let before = current.childNodes[position]
kxi.addPatch(pkInsertBefore, current, before, kid, nil)
parent.insert(kid, position)
applyPatch(kxi)
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) =
let olddom = oldNode.dom
doAssert olddom != nil
diff(newNode, oldNode, nil, olddom, kxi)
# this is a bit nasty: Since we cannot patch the 'parent' of
# the current VNode (because we don't store it at all!), we
# need to override the fields individually:
takeOverFields(newNode, oldNode)
applyComponents(kxi)
applyPatch(kxi)
if kxi.currentTree == oldNode:
kxi.currentTree = newNode
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
var onhashChange {.importc: "window.onhashchange".}: proc()
var hashPart {.importc: "window.location.hash".}: cstring
proc avoidDomDiffing*(kxi: KaraxInstance = kxi) =
## enforce a full redraw for the next redraw operation.
## This can be used as a temporary way to workaround DOM diffing
## problems or to avoid the DOM diffing when you already know
## it should use a completely new DOM.
## This is an experimental API.
kxi.currentTree = nil
proc reqFrame(callback: proc()): int {.importc: "window.requestAnimationFrame".}
when false:
proc cancelFrame(id: int) {.importc: "window.cancelAnimationFrame".}
proc dodraw(kxi: KaraxInstance) =
if kxi.renderer.isNil: return
kxi.renderId = 0
if kxi.rendering:
# there is a render already running, delay 1 frame
kxi.renderId = reqFrame(proc () = kxi.dodraw)
return
kxi.rendering = true
let rdata = RouterData(hashPart: hashPart)
let newtree = kxi.renderer(rdata)
inc kxi.runCount
newtree.id = kxi.rootId
kxi.toFocus = nil
if kxi.currentTree == nil:
let asdom = toDom(newtree, useAttachedNode = true, kxi)
replaceById(kxi.rootId, asdom)
else:
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
let olddom = document.getElementById(kxi.rootId)
diff(newtree, kxi.currentTree, nil, olddom, kxi)
when defined(profileKarax):
echo "<<<<<<<<<<<<<<"
echa reasons
applyComponents(kxi)
when defined(profileKarax):
echo "--------------"
echa reasons
echo ">>>>>>>>>>>>>>"
applyPatch(kxi)
kxi.currentTree = newtree
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
if not kxi.postRenderCallback.isNil:
kxi.postRenderCallback(rdata)
# now that it's part of the DOM, give it the focus:
if kxi.toFocus != nil:
kxi.toFocus.focus()
kxi.rendering = false
when defined(stats):
kxi.recursion = 0
var total = 0
echo "depth ", depth(kxi.currentTree, total), " total ", total
proc redraw*(kxi: KaraxInstance = kxi) =
# we buffer redraw requests:
when false:
if drawTimeout != nil:
clearTimeout(drawTimeout)
drawTimeout = setTimeout(dodraw, 30)
elif true:
if kxi.renderId == 0:
kxi.renderId = reqFrame(proc () = kxi.dodraw)
else:
dodraw(kxi)
proc redrawSync*(kxi: KaraxInstance = kxi) = dodraw(kxi)
proc init(ev: Event) =
kxi.renderId = reqFrame(proc () = kxi.dodraw)
proc setRenderer*(renderer: proc (data: RouterData): VNode,
root: cstring = "ROOT",
clientPostRenderCallback:
proc (data: RouterData) = nil): KaraxInstance {.
discardable.} =
## Setup Karax. Usually the return value can be ignored.
if document.getElementById(root).isNil:
let msg = "Could not find a <div> with id=" & root &
". Karax needs it as its rendering target."
raise newException(Exception, $msg)
result = KaraxInstance(rootId: root, renderer: renderer,
postRenderCallback: clientPostRenderCallback,
patches: newSeq[Patch](60),
patchesV: newSeq[PatchV](30),
components: @[],
surpressRedraws: false,
byId: newJDict[cstring, VNode](),
orphans: newJDict[cstring, bool]())
kxi = result
window.onload = init
onhashChange = proc() = redraw()
proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT",
clientPostRenderCallback: proc () = nil): KaraxInstance {.discardable.} =
## Setup Karax. Usually the return value can be ignored.
proc wrapRenderer(data: RouterData): VNode = result = renderer()
proc wrapPostRender(data: RouterData) =
if clientPostRenderCallback != nil: clientPostRenderCallback()
setRenderer(wrapRenderer, root, wrapPostRender)
proc setInitializer*(renderer: proc (data: RouterData): VNode, root: cstring = "ROOT",
clientPostRenderCallback:
proc (data: RouterData) = nil): KaraxInstance {.discardable.} =
## Setup Karax. Usually the return value can be ignored.
result = KaraxInstance(rootId: root, renderer: renderer,
postRenderCallback: clientPostRenderCallback,
patches: newSeq[Patch](60),
patchesV: newSeq[PatchV](30),
components: @[],
surpressRedraws: true,
byId: newJDict[cstring, VNode](),
orphans: newJDict[cstring, bool]())
kxi = result
window.onload = init
proc addEventHandler*(n: VNode; k: EventKind; action: EventHandler;
kxi: KaraxInstance = kxi) =
## Implements the foundation of Karax's event management.
## Karax DSL transforms ``tag(onEvent = handler)`` to
## ``tempNode.addEventHandler(tagNode, EventKind.onEvent, wrapper)``
## where ``wrapper`` calls the passed ``action`` and then triggers
## a ``redraw``.
proc wrapper(ev: Event; n: VNode) =
action(ev, n)
if not kxi.surpressRedraws: redraw(kxi)
addEventListener(n, k, wrapper)
proc addEventHandler*(n: VNode; k: EventKind; action: proc();
kxi: KaraxInstance = kxi) =
## Implements the foundation of Karax's event management.
## Karax DSL transforms ``tag(onEvent = handler)`` to
## ``tempNode.addEventHandler(tagNode, EventKind.onEvent, wrapper)``
## where ``wrapper`` calls the passed ``action`` and then triggers
## a ``redraw``.
proc wrapper(ev: Event; n: VNode) =
action()
if not kxi.surpressRedraws: redraw(kxi)
addEventListener(n, k, wrapper)
proc setOnHashChange*(action: proc (hashPart: cstring)) {.deprecated: "use setRenderer instead".} =
## Now deprecated, instead pass a callback to ``setRenderer`` that receives
## a ``data: RouterData`` parameter.
proc wrapper() =
action(hashPart)
redraw()
onhashchange = wrapper
proc setForeignNodeId*(id: cstring; kxi: KaraxInstance = kxi) =
## Declares a node ID as "foreign". Foreign nodes are not
## under Karax's control in the sense that Karax does not attempt
## to perform structural checks on them.
kxi.orphans[id] = true
{.push stackTrace:off.}
proc setupErrorHandler*() =
## Installs an error handler that transforms native JS unhandled
## exceptions into Nim based stack traces. If `useAlert` is false,
## the error message is put into the console, otherwise `alert`
## is called.
proc stackTraceAsCstring(): cstring = cstring(getStackTrace())
var onerror {.importc: "window.onerror", used.} =
proc (msg, url: cstring, line, col: int, error: cstring): bool =
var x = cstring"Error: " & msg & "\n" & stackTraceAsCstring()
echo(x)
return true # suppressErrorAlert
{.pop.}
proc prepend(parent, kid: Element) =
parent.insertBefore(kid, parent.firstChild)
proc loadScript*(jsfilename: cstring; kxi: KaraxInstance = kxi) =
let s = document.createElement("script")
s.setAttr "type", "text/javascript"
s.setAttr "src", jsfilename
document.body.prepend(s)
redraw(kxi)
proc runLater*(action: proc(); later = 400): Timeout {.discardable.} =
proc wrapper() =
action()
redraw()
result = setTimeout(wrapper, later)
proc setInputText*(n: VNode; s: cstring) =
## Sets the text of input elements.
n.text = s
if n.dom != nil: n.dom.value = s
proc getInputText*(n: VNode): cstring =
if n.dom != nil:
result = n.dom.value
proc toChecked*(checked: bool): cstring =
(if checked: cstring"checked" else: cstring(nil))
proc toDisabled*(disabled: bool): cstring =
(if disabled: cstring"disabled" else: cstring(nil))
proc toSelected*(selected: bool): cstring =
(if selected: cstring"selected" else: cstring(nil))
proc vnodeToDom*(n: VNode; kxi: KaraxInstance = nil): Node =
result = toDom(n, useAttachedNode = false, kxi)

200
karax/karaxdsl.nim Normal file
View file

@ -0,0 +1,200 @@
import macros, vdom, compact, kbase
from strutils import startsWith, toLowerAscii
when defined(js):
import karax
const
StmtContext = ["kout", "inc", "echo", "dec", "!"]
SpecialAttrs = ["id", "class", "value", "index", "style"]
proc getName(n: NimNode): string =
case n.kind
of nnkIdent, nnkSym:
result = $n
of nnkAccQuoted:
result = ""
for i in 0..<n.len:
result.add getName(n[i])
of nnkStrLit..nnkTripleStrLit:
result = n.strVal
of nnkInfix:
# allow 'foo-bar' syntax:
if n.len == 3 and $n[0] == "-":
result = getName(n[1]) & "-" & getName(n[2])
else:
expectKind(n, nnkIdent)
of nnkDotExpr:
result = getName(n[0]) & "." & getName(n[1])
of nnkOpenSymChoice, nnkClosedSymChoice:
result = getName(n[0])
else:
#echo repr n
expectKind(n, nnkIdent)
proc toKstring(n: NimNode): NimNode =
if n.kind == nnkStrLit:
result = newCall(bindSym"kstring", n)
else:
result = copyNimNode(n)
for child in n:
result.add toKstring(child)
proc newDotAsgn(tmp: NimNode, key: string, x: NimNode): NimNode =
result = newTree(nnkAsgn, newDotExpr(tmp, newIdentNode key), x)
proc tcall2(n, tmpContext: NimNode): NimNode =
# we need to distinguish statement and expression contexts:
# every call statement 's' needs to be transformed to 'dest.add s'.
# If expressions need to be distinguished from if statements. Since
# we know we start in a statement context, it's pretty simple to
# figure out expression contexts: In calls everything is an expression
# (except for the last child of the macros we consider here),
# lets, consts, types can be considered as expressions
# case is complex, calls are assumed to produce a value.
when defined(js):
template evHandler(): untyped = bindSym"addEventHandler"
else:
template evHandler(): untyped = ident"addEventHandler"
case n.kind
of nnkLiterals, nnkIdent, nnkSym, nnkDotExpr, nnkBracketExpr:
if tmpContext != nil:
result = newCall(bindSym"add", tmpContext, n)
else:
result = n
of nnkForStmt, nnkIfExpr, nnkElifExpr, nnkElseExpr,
nnkOfBranch, nnkElifBranch, nnkExceptBranch, nnkElse,
nnkConstDef, nnkWhileStmt, nnkIdentDefs, nnkVarTuple:
# recurse for the last son:
result = copyNimTree(n)
let L = n.len
assert n.len == result.len
if L > 0:
result[L-1] = tcall2(result[L-1], tmpContext)
of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkTryStmt,
nnkFinally:
# recurse for every child:
result = copyNimNode(n)
for x in n:
result.add tcall2(x, tmpContext)
of nnkCaseStmt:
# recurse for children, but don't add call for case ident
result = copyNimNode(n)
result.add n[0]
for i in 1 ..< n.len:
result.add tcall2(n[i], tmpContext)
of nnkProcDef:
let name = getName n[0]
if name.startsWith"on":
# turn it into an anon proc:
let anon = copyNimTree(n)
anon[0] = newEmptyNode()
if tmpContext == nil:
error "no VNode to attach the event handler to"
else:
result = newCall(evHandler(), tmpContext,
newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi"))
else:
result = n
of nnkVarSection, nnkLetSection, nnkConstSection:
result = n
of nnkCallKinds - {nnkInfix}:
let op = getName(n[0])
let ck = isComponent(op)
if ck != ComponentKind.None:
let tmp = genSym(nskLet, "tmp")
let call = if ck == ComponentKind.Tag:
newCall(bindSym"tree", newDotExpr(bindSym"VNodeKind", n[0]))
elif ck == ComponentKind.VNode:
newCall(bindSym"vthunk", newLit(op))
else:
newCall(bindSym"dthunk", newLit(op))
result = newTree(
if tmpContext == nil: nnkStmtListExpr else: nnkStmtList,
newLetStmt(tmp, call))
for i in 1 ..< n.len:
# named parameters are transformed into attributes or events:
let x = n[i]
if x.kind == nnkExprEqExpr:
let key = getName x[0]
if key.startsWith("on"):
result.add newCall(evHandler(),
tmp, newDotExpr(bindSym"EventKind", x[0]), x[1], ident("kxi"))
elif eqIdent(key, "style") and x[1].kind == nnkTableConstr:
result.add newDotAsgn(tmp, key, newCall("style", toKstring x[1]))
elif key in SpecialAttrs:
result.add newDotAsgn(tmp, key, x[1])
if key == "value":
result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
elif eqIdent(key, "setFocus"):
result.add newCall(key, tmp, x[1], ident"kxi")
elif eqIdent(key, "events"):
result.add newCall(bindSym"mergeEvents", tmp, x[1])
else:
result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
elif ck != ComponentKind.Tag:
call.add x
elif eqIdent(x, "setFocus"):
result.add newCall(x, tmp, bindSym"true", ident"kxi")
else:
result.add tcall2(x, tmp)
if tmpContext == nil:
result.add tmp
else:
result.add newCall(bindSym"add", tmpContext, tmp)
elif tmpContext != nil and op notin StmtContext:
var hasEventHandlers = false
for i in 1..<n.len:
let it = n[i]
if it.kind in {nnkProcDef, nnkStmtList}:
hasEventHandlers = true
break
if not hasEventHandlers:
result = newCall(bindSym"add", tmpContext, n)
else:
let tmp = genSym(nskLet, "tmp")
var slicedCall = newCall(n[0])
let ex = newTree(nnkStmtListExpr)
ex.add newEmptyNode() # will become the let statement
for i in 1..<n.len:
let it = n[i]
if it.kind in {nnkProcDef, nnkStmtList}:
ex.add tcall2(it, tmp)
else:
slicedCall.add it
ex[0] = newLetStmt(tmp, slicedCall)
ex.add tmp
result = newCall(bindSym"add", tmpContext, ex)
elif op == "!" and n.len == 2:
result = n[1]
else:
result = n
else:
result = n
macro buildHtml*(tag, children: untyped): VNode =
let kids = newProc(procType=nnkDo, body=children)
expectKind kids, nnkDo
var call: NimNode
if tag.kind in nnkCallKinds:
call = tag
else:
call = newCall(tag)
call.add body(kids)
result = tcall2(call, nil)
when defined(debugKaraxDsl):
echo repr result
macro buildHtml*(children: untyped): VNode =
let kids = newProc(procType=nnkDo, body=children)
expectKind kids, nnkDo
result = tcall2(body(kids), nil)
when defined(debugKaraxDsl):
echo repr result
macro flatHtml*(tag: untyped): VNode =
result = tcall2(tag, nil)
when defined(debugKaraxDsl):
echo repr result

8
karax/kbase.nim Normal file
View file

@ -0,0 +1,8 @@
## Karax -- Single page applications for Nim.
## Basic type definitions for Karax.
when defined(js):
type kstring* = cstring
else:
type kstring* = string

10
karax/kdom.nim Normal file
View file

@ -0,0 +1,10 @@
## Karax -- Single page applications for Nim.
from dom import nil
when not declared(dom.DomApiVersion):
include kdom_impl
else:
import dom
export dom

1232
karax/kdom_impl.nim Normal file

File diff suppressed because it is too large Load diff

616
karax/languages.nim Normal file
View file

@ -0,0 +1,616 @@
## Karax -- Single page applications for Nim.
type
Language* {.pure.} = enum ## A pure enum that contains most (all?)
## known human languages in current use.
afZA ## "South Africa - Afrikaans"
amET ## "Ethiopia - Amharic"
arAE ## "U.A.E. - Arabic"
arBH ## "Bahrain - Arabic"
arDZ ## "Algeria - Arabic"
arEG ## "Egypt - Arabic"
arIQ ## "Iraq - Arabic"
arJO ## "Jordan - Arabic"
arKW ## "Kuwait - Arabic"
arLB ## "Lebanon - Arabic"
arLY ## "Libya - Arabic"
arMA ## "Morocco - Arabic"
arOM ## "Oman - Arabic"
arQA ## "Qatar - Arabic"
arSA ## "Saudi Arabia - Arabic"
arSY ## "Syria - Arabic"
arTN ## "Tunisia - Arabic"
arYE ## "Yemen - Arabic"
arnCL ## "Chile - Mapudungun"
asIN ## "India - Assamese"
azAZ ## "Azerbaijan - Azeri (Cyrillic)"
baRU ## "Russia - Bashkir"
beBY ## "Belarus - Belarusian"
bgBG ## "Bulgaria - Bulgarian"
bnBD ## "Bangladesh - Bengali"
bnIN ## "India - Bengali"
boCN ## "People's Republic of China - Tibetan"
brFR ## "France - Breton"
bsBA ## "Bosnia and Herzegovina - Bosnian (Cyrillic)"
caES ## "Spain - Catalan"
coFR ## "France - Corsican"
csCZ ## "Czech Republic - Czech"
cyGB ## "United Kingdom - Welsh"
daDK ## "Denmark - Danish"
deAT ## "Austria - German"
deCH ## "Switzerland - German"
deDE ## "Germany - German"
deLI ## "Liechtenstein - German"
deLU ## "Luxembourg - German"
dsbDE ## "Germany - Lower Sorbian"
dvMV ## "Maldives - Divehi"
elGR ## "Greece - Greek"
en029 ## "Caribbean - English"
enAU ## "Australia - English"
enBZ ## "Belize - English"
enCA ## "Canada - English"
enGB ## "United Kingdom - English"
enIE ## "Ireland - English"
enIN ## "India - English"
enJM ## "Jamaica - English"
enMY ## "Malaysia - English"
enNZ ## "New Zealand - English"
enPH ## "Republic of the Philippines - English"
enSG ## "Singapore - English"
enTT ## "Trinidad and Tobago - English"
enUS ## "United States - English"
enZA ## "South Africa - English"
enZW ## "Zimbabwe - English"
esAR ## "Argentina - Spanish"
esBO ## "Bolivia - Spanish"
esCL ## "Chile - Spanish"
esCO ## "Colombia - Spanish"
esCR ## "Costa Rica - Spanish"
esDO ## "Dominican Republic - Spanish"
esEC ## "Ecuador - Spanish"
esES ## "Spain - Spanish"
esGT ## "Guatemala - Spanish"
esHN ## "Honduras - Spanish"
esMX ## "Mexico - Spanish"
esNI ## "Nicaragua - Spanish"
esPA ## "Panama - Spanish"
esPE ## "Peru - Spanish"
esPR ## "Puerto Rico - Spanish"
esPY ## "Paraguay - Spanish"
esSV ## "El Salvador - Spanish"
esUS ## "United States - Spanish"
esUY ## "Uruguay - Spanish"
esVE ## "Bolivarian Republic of Venezuela - Spanish"
etEE ## "Estonia - Estonian"
euES ## "Spain - Basque"
faIR ## "Iran - Persian"
fiFI ## "Finland - Finnish"
filPH ## "Philippines - Filipino"
foFO ## "Faroe Islands - Faroese"
frBE ## "Belgium - French"
frCA ## "Canada - French"
frCH ## "Switzerland - French"
frFR ## "France - French"
frLU ## "Luxembourg - French"
frMC ## "Principality of Monaco - French"
fyNL ## "Netherlands - Frisian"
gaIE ## "Ireland - Irish"
gdGB ## "United Kingdom - Scottish Gaelic"
glES ## "Spain - Galician"
gswFR ## "France - Alsatian"
guIN ## "India - Gujarati"
haNG ## "Nigeria - Hausa (Latin)"
heIL ## "Israel - Hebrew"
hiIN ## "India - Hindi"
hrBA ## "Bosnia and Herzegovina - Croatian"
hrHR ## "Croatia - Croatian"
hsbDE ## "Germany - Upper Sorbian"
huHU ## "Hungary - Hungarian"
hyAM ## "Armenia - Armenian"
idID ## "Indonesia - Indonesian"
igNG ## "Nigeria - Igbo"
iiCN ## "People's Republic of China - Yi"
isIS ## "Iceland - Icelandic"
itCH ## "Switzerland - Italian"
itIT ## "Italy - Italian"
iuCA ## "Canada - Inuktitut (Latin)"
jaJP ## "Japan - Japanese"
kaGE ## "Georgia - Georgian"
kkKZ ## "Kazakhstan - Kazakh"
klGL ## "Greenland - Greenlandic"
kmKH ## "Cambodia - Khmer"
knIN ## "India - Kannada"
koKR ## "Korea - Korean"
kokIN ## "India - Konkani"
kyKG ## "Kyrgyzstan - Kyrgyz"
lbLU ## "Luxembourg - Luxembourgish"
loLA ## "Lao P.D.R. - Lao"
ltLT ## "Lithuania - Lithuanian"
lvLV ## "Latvia - Latvian"
miNZ ## "New Zealand - Maori"
mkMK ## "Macedonia (FYROM) - Macedonian (FYROM)"
mlIN ## "India - Malayalam"
mnCN ## "People's Republic of China - Mongolian (Traditional Mongolian)"
mnMN ## "Mongolia - Mongolian (Cyrillic)"
mohCA ## "Canada - Mohawk"
mrIN ## "India - Marathi"
msBN ## "Brunei Darussalam - Malay"
msMY ## "Malaysia - Malay"
mtMT ## "Malta - Maltese"
nbNO ## "Norway - Norwegian (Bokmal)"
neNP ## "Nepal - Nepali"
nlBE ## "Belgium - Dutch"
nlNL ## "Netherlands - Dutch"
nnNO ## "Norway - Norwegian (Nynorsk)"
nsoZA ## "South Africa - Sesotho sa Leboa"
ocFR ## "France - Occitan"
orIN ## "India - Oriya"
paIN ## "India - Punjabi"
plPL ## "Poland - Polish"
prsAF ## "Afghanistan - Dari"
psAF ## "Afghanistan - Pashto"
ptBR ## "Brazil - Portuguese"
ptPT ## "Portugal - Portuguese"
qutGT ## "Guatemala - K'iche"
quzBO ## "Bolivia - Quechua"
quzEC ## "Ecuador - Quechua"
quzPE ## "Peru - Quechua"
rmCH ## "Switzerland - Romansh"
roRO ## "Romania - Romanian"
ruRU ## "Russia - Russian"
rwRW ## "Rwanda - Kinyarwanda"
saIN ## "India - Sanskrit"
sahRU ## "Russia - Yakut"
seFI ## "Finland - Sami (Northern)"
seNO ## "Norway - Sami (Northern)"
seSE ## "Sweden - Sami (Northern)"
siLK ## "Sri Lanka - Sinhala"
skSK ## "Slovakia - Slovak"
slSI ## "Slovenia - Slovenian"
smaNO ## "Norway - Sami (Southern)"
smaSE ## "Sweden - Sami (Southern)"
smjNO ## "Norway - Sami (Lule)"
smjSE ## "Sweden - Sami (Lule)"
smnFI ## "Finland - Sami (Inari)"
smsFI ## "Finland - Sami (Skolt)"
sqAL ## "Albania - Albanian"
srBA ## "Bosnia and Herzegovina - Serbian (Cyrillic)"
srCS ## "Serbia and Montenegro (Former) - Serbian (Cyrillic)"
srME ## "Montenegro - Serbian (Cyrillic)"
srRS ## "Serbia - Serbian (Cyrillic)"
svFI ## "Finland - Swedish"
svSE ## "Sweden - Swedish"
swKE ## "Kenya - Kiswahili"
syrSY ## "Syria - Syriac"
taIN ## "India - Tamil"
teIN ## "India - Telugu"
tgTJ ## "Tajikistan - Tajik (Cyrillic)"
thTH ## "Thailand - Thai"
tkTM ## "Turkmenistan - Turkmen"
tnZA ## "South Africa - Setswana"
trTR ## "Turkey - Turkish"
ttRU ## "Russia - Tatar"
tzmDZ ## "Algeria - Tamazight (Latin)"
ugCN ## "People's Republic of China - Uyghur"
ukUA ## "Ukraine - Ukrainian"
urPK ## "Islamic Republic of Pakistan - Urdu"
uzUZ ## "Uzbekistan - Uzbek (Cyrillic)"
viVN ## "Vietnam - Vietnamese"
woSN ## "Senegal - Wolof"
xhZA ## "South Africa - isiXhosa"
yoNG ## "Nigeria - Yoruba"
zhCN ## "People's Republic of China - Chinese (Simplified) Legacy"
zhHK ## "Hong Kong S.A.R. - Chinese (Traditional) Legacy"
zhMO ## "Macao S.A.R. - Chinese (Traditional) Legacy"
zhSG ## "Singapore - Chinese (Simplified) Legacy"
zhTW ## "Taiwan - Chinese (Traditional) Legacy"
zuZA ## "South Africa - isiZulu"
const
languageToName*: array[Language, cstring] = [
Language.afZA: cstring"South Africa - Afrikaans",
Language.amET: cstring"Ethiopia - Amharic",
Language.arAE: cstring"U.A.E. - Arabic",
Language.arBH: cstring"Bahrain - Arabic",
Language.arDZ: cstring"Algeria - Arabic",
Language.arEG: cstring"Egypt - Arabic",
Language.arIQ: cstring"Iraq - Arabic",
Language.arJO: cstring"Jordan - Arabic",
Language.arKW: cstring"Kuwait - Arabic",
Language.arLB: cstring"Lebanon - Arabic",
Language.arLY: cstring"Libya - Arabic",
Language.arMA: cstring"Morocco - Arabic",
Language.arOM: cstring"Oman - Arabic",
Language.arQA: cstring"Qatar - Arabic",
Language.arSA: cstring"Saudi Arabia - Arabic",
Language.arSY: cstring"Syria - Arabic",
Language.arTN: cstring"Tunisia - Arabic",
Language.arYE: cstring"Yemen - Arabic",
Language.arnCL: cstring"Chile - Mapudungun",
Language.asIN: cstring"India - Assamese",
Language.azAZ: cstring"Azerbaijan - Azeri (Cyrillic)",
Language.baRU: cstring"Russia - Bashkir",
Language.beBY: cstring"Belarus - Belarusian",
Language.bgBG: cstring"Bulgaria - Bulgarian",
Language.bnBD: cstring"Bangladesh - Bengali",
Language.bnIN: cstring"India - Bengali",
Language.boCN: cstring"People's Republic of China - Tibetan",
Language.brFR: cstring"France - Breton",
Language.bsBA: cstring"Bosnia and Herzegovina - Bosnian (Cyrillic)",
Language.caES: cstring"Spain - Catalan",
Language.coFR: cstring"France - Corsican",
Language.csCZ: cstring"Czech Republic - Czech",
Language.cyGB: cstring"United Kingdom - Welsh",
Language.daDK: cstring"Denmark - Danish",
Language.deAT: cstring"Austria - German",
Language.deCH: cstring"Switzerland - German",
Language.deDE: cstring"Germany - German",
Language.deLI: cstring"Liechtenstein - German",
Language.deLU: cstring"Luxembourg - German",
Language.dsbDE: cstring"Germany - Lower Sorbian",
Language.dvMV: cstring"Maldives - Divehi",
Language.elGR: cstring"Greece - Greek",
Language.en029: cstring"Caribbean - English",
Language.enAU: cstring"Australia - English",
Language.enBZ: cstring"Belize - English",
Language.enCA: cstring"Canada - English",
Language.enGB: cstring"United Kingdom - English",
Language.enIE: cstring"Ireland - English",
Language.enIN: cstring"India - English",
Language.enJM: cstring"Jamaica - English",
Language.enMY: cstring"Malaysia - English",
Language.enNZ: cstring"New Zealand - English",
Language.enPH: cstring"Republic of the Philippines - English",
Language.enSG: cstring"Singapore - English",
Language.enTT: cstring"Trinidad and Tobago - English",
Language.enUS: cstring"United States - English",
Language.enZA: cstring"South Africa - English",
Language.enZW: cstring"Zimbabwe - English",
Language.esAR: cstring"Argentina - Spanish",
Language.esBO: cstring"Bolivia - Spanish",
Language.esCL: cstring"Chile - Spanish",
Language.esCO: cstring"Colombia - Spanish",
Language.esCR: cstring"Costa Rica - Spanish",
Language.esDO: cstring"Dominican Republic - Spanish",
Language.esEC: cstring"Ecuador - Spanish",
Language.esES: cstring"Spain - Spanish",
Language.esGT: cstring"Guatemala - Spanish",
Language.esHN: cstring"Honduras - Spanish",
Language.esMX: cstring"Mexico - Spanish",
Language.esNI: cstring"Nicaragua - Spanish",
Language.esPA: cstring"Panama - Spanish",
Language.esPE: cstring"Peru - Spanish",
Language.esPR: cstring"Puerto Rico - Spanish",
Language.esPY: cstring"Paraguay - Spanish",
Language.esSV: cstring"El Salvador - Spanish",
Language.esUS: cstring"United States - Spanish",
Language.esUY: cstring"Uruguay - Spanish",
Language.esVE: cstring"Bolivarian Republic of Venezuela - Spanish",
Language.etEE: cstring"Estonia - Estonian",
Language.euES: cstring"Spain - Basque",
Language.faIR: cstring"Iran - Persian",
Language.fiFI: cstring"Finland - Finnish",
Language.filPH: cstring"Philippines - Filipino",
Language.foFO: cstring"Faroe Islands - Faroese",
Language.frBE: cstring"Belgium - French",
Language.frCA: cstring"Canada - French",
Language.frCH: cstring"Switzerland - French",
Language.frFR: cstring"France - French",
Language.frLU: cstring"Luxembourg - French",
Language.frMC: cstring"Principality of Monaco - French",
Language.fyNL: cstring"Netherlands - Frisian",
Language.gaIE: cstring"Ireland - Irish",
Language.gdGB: cstring"United Kingdom - Scottish Gaelic",
Language.glES: cstring"Spain - Galician",
Language.gswFR: cstring"France - Alsatian",
Language.guIN: cstring"India - Gujarati",
Language.haNG: cstring"Nigeria - Hausa (Latin)",
Language.heIL: cstring"Israel - Hebrew",
Language.hiIN: cstring"India - Hindi",
Language.hrBA: cstring"Bosnia and Herzegovina - Croatian",
Language.hrHR: cstring"Croatia - Croatian",
Language.hsbDE: cstring"Germany - Upper Sorbian",
Language.huHU: cstring"Hungary - Hungarian",
Language.hyAM: cstring"Armenia - Armenian",
Language.idID: cstring"Indonesia - Indonesian",
Language.igNG: cstring"Nigeria - Igbo",
Language.iiCN: cstring"People's Republic of China - Yi",
Language.isIS: cstring"Iceland - Icelandic",
Language.itCH: cstring"Switzerland - Italian",
Language.itIT: cstring"Italy - Italian",
Language.iuCA: cstring"Canada - Inuktitut (Latin)",
Language.jaJP: cstring"Japan - Japanese",
Language.kaGE: cstring"Georgia - Georgian",
Language.kkKZ: cstring"Kazakhstan - Kazakh",
Language.klGL: cstring"Greenland - Greenlandic",
Language.kmKH: cstring"Cambodia - Khmer",
Language.knIN: cstring"India - Kannada",
Language.koKR: cstring"Korea - Korean",
Language.kokIN: cstring"India - Konkani",
Language.kyKG: cstring"Kyrgyzstan - Kyrgyz",
Language.lbLU: cstring"Luxembourg - Luxembourgish",
Language.loLA: cstring"Lao P.D.R. - Lao",
Language.ltLT: cstring"Lithuania - Lithuanian",
Language.lvLV: cstring"Latvia - Latvian",
Language.miNZ: cstring"New Zealand - Maori",
Language.mkMK: cstring"Macedonia (FYROM) - Macedonian (FYROM)",
Language.mlIN: cstring"India - Malayalam",
Language.mnCN: cstring"People's Republic of China - Mongolian (Traditional Mongolian)",
Language.mnMN: cstring"Mongolia - Mongolian (Cyrillic)",
Language.mohCA: cstring"Canada - Mohawk",
Language.mrIN: cstring"India - Marathi",
Language.msBN: cstring"Brunei Darussalam - Malay",
Language.msMY: cstring"Malaysia - Malay",
Language.mtMT: cstring"Malta - Maltese",
Language.nbNO: cstring"Norway - Norwegian (Bokmal)",
Language.neNP: cstring"Nepal - Nepali",
Language.nlBE: cstring"Belgium - Dutch",
Language.nlNL: cstring"Netherlands - Dutch",
Language.nnNO: cstring"Norway - Norwegian (Nynorsk)",
Language.nsoZA: cstring"South Africa - Sesotho sa Leboa",
Language.ocFR: cstring"France - Occitan",
Language.orIN: cstring"India - Oriya",
Language.paIN: cstring"India - Punjabi",
Language.plPL: cstring"Poland - Polish",
Language.prsAF: cstring"Afghanistan - Dari",
Language.psAF: cstring"Afghanistan - Pashto",
Language.ptBR: cstring"Brazil - Portuguese",
Language.ptPT: cstring"Portugal - Portuguese",
Language.qutGT: cstring"Guatemala - K'iche",
Language.quzBO: cstring"Bolivia - Quechua",
Language.quzEC: cstring"Ecuador - Quechua",
Language.quzPE: cstring"Peru - Quechua",
Language.rmCH: cstring"Switzerland - Romansh",
Language.roRO: cstring"Romania - Romanian",
Language.ruRU: cstring"Russia - Russian",
Language.rwRW: cstring"Rwanda - Kinyarwanda",
Language.saIN: cstring"India - Sanskrit",
Language.sahRU: cstring"Russia - Yakut",
Language.seFI: cstring"Finland - Sami (Northern)",
Language.seNO: cstring"Norway - Sami (Northern)",
Language.seSE: cstring"Sweden - Sami (Northern)",
Language.siLK: cstring"Sri Lanka - Sinhala",
Language.skSK: cstring"Slovakia - Slovak",
Language.slSI: cstring"Slovenia - Slovenian",
Language.smaNO: cstring"Norway - Sami (Southern)",
Language.smaSE: cstring"Sweden - Sami (Southern)",
Language.smjNO: cstring"Norway - Sami (Lule)",
Language.smjSE: cstring"Sweden - Sami (Lule)",
Language.smnFI: cstring"Finland - Sami (Inari)",
Language.smsFI: cstring"Finland - Sami (Skolt)",
Language.sqAL: cstring"Albania - Albanian",
Language.srBA: cstring"Bosnia and Herzegovina - Serbian (Cyrillic)",
Language.srCS: cstring"Serbia and Montenegro (Former) - Serbian (Cyrillic)",
Language.srME: cstring"Montenegro - Serbian (Cyrillic)",
Language.srRS: cstring"Serbia - Serbian (Cyrillic)",
Language.svFI: cstring"Finland - Swedish",
Language.svSE: cstring"Sweden - Swedish",
Language.swKE: cstring"Kenya - Kiswahili",
Language.syrSY: cstring"Syria - Syriac",
Language.taIN: cstring"India - Tamil",
Language.teIN: cstring"India - Telugu",
Language.tgTJ: cstring"Tajikistan - Tajik (Cyrillic)",
Language.thTH: cstring"Thailand - Thai",
Language.tkTM: cstring"Turkmenistan - Turkmen",
Language.tnZA: cstring"South Africa - Setswana",
Language.trTR: cstring"Turkey - Turkish",
Language.ttRU: cstring"Russia - Tatar",
Language.tzmDZ: cstring"Algeria - Tamazight (Latin)",
Language.ugCN: cstring"People's Republic of China - Uyghur",
Language.ukUA: cstring"Ukraine - Ukrainian",
Language.urPK: cstring"Islamic Republic of Pakistan - Urdu",
Language.uzUZ: cstring"Uzbekistan - Uzbek (Cyrillic)",
Language.viVN: cstring"Vietnam - Vietnamese",
Language.woSN: cstring"Senegal - Wolof",
Language.xhZA: cstring"South Africa - isiXhosa",
Language.yoNG: cstring"Nigeria - Yoruba",
Language.zhCN: cstring"People's Republic of China - Chinese (Simplified) Legacy",
Language.zhHK: cstring"Hong Kong S.A.R. - Chinese (Traditional) Legacy",
Language.zhMO: cstring"Macao S.A.R. - Chinese (Traditional) Legacy",
Language.zhSG: cstring"Singapore - Chinese (Simplified) Legacy",
Language.zhTW: cstring"Taiwan - Chinese (Traditional) Legacy",
Language.zuZA: cstring"South Africa - isiZulu"]
languageToCode*: array[Language, cstring] = [
Language.afZA: cstring"af-ZA",
Language.amET: cstring"am-ET",
Language.arAE: cstring"ar-AE",
Language.arBH: cstring"ar-BH",
Language.arDZ: cstring"ar-DZ",
Language.arEG: cstring"ar-EG",
Language.arIQ: cstring"ar-IQ",
Language.arJO: cstring"ar-JO",
Language.arKW: cstring"ar-KW",
Language.arLB: cstring"ar-LB",
Language.arLY: cstring"ar-LY",
Language.arMA: cstring"ar-MA",
Language.arOM: cstring"ar-OM",
Language.arQA: cstring"ar-QA",
Language.arSA: cstring"ar-SA",
Language.arSY: cstring"ar-SY",
Language.arTN: cstring"ar-TN",
Language.arYE: cstring"ar-YE",
Language.arnCL: cstring"arn-CL",
Language.asIN: cstring"as-IN",
Language.azAZ: cstring"az-AZ",
Language.baRU: cstring"ba-RU",
Language.beBY: cstring"be-BY",
Language.bgBG: cstring"bg-BG",
Language.bnBD: cstring"bn-BD",
Language.bnIN: cstring"bn-IN",
Language.boCN: cstring"bo-CN",
Language.brFR: cstring"br-FR",
Language.bsBA: cstring"bs-BA",
Language.caES: cstring"ca-ES",
Language.coFR: cstring"co-FR",
Language.csCZ: cstring"cs-CZ",
Language.cyGB: cstring"cy-GB",
Language.daDK: cstring"da-DK",
Language.deAT: cstring"de-AT",
Language.deCH: cstring"de-CH",
Language.deDE: cstring"de-DE",
Language.deLI: cstring"de-LI",
Language.deLU: cstring"de-LU",
Language.dsbDE: cstring"dsb-DE",
Language.dvMV: cstring"dv-MV",
Language.elGR: cstring"el-GR",
Language.en029: cstring"en-029",
Language.enAU: cstring"en-AU",
Language.enBZ: cstring"en-BZ",
Language.enCA: cstring"en-CA",
Language.enGB: cstring"en-GB",
Language.enIE: cstring"en-IE",
Language.enIN: cstring"en-IN",
Language.enJM: cstring"en-JM",
Language.enMY: cstring"en-MY",
Language.enNZ: cstring"en-NZ",
Language.enPH: cstring"en-PH",
Language.enSG: cstring"en-SG",
Language.enTT: cstring"en-TT",
Language.enUS: cstring"en-US",
Language.enZA: cstring"en-ZA",
Language.enZW: cstring"en-ZW",
Language.esAR: cstring"es-AR",
Language.esBO: cstring"es-BO",
Language.esCL: cstring"es-CL",
Language.esCO: cstring"es-CO",
Language.esCR: cstring"es-CR",
Language.esDO: cstring"es-DO",
Language.esEC: cstring"es-EC",
Language.esES: cstring"es-ES",
Language.esGT: cstring"es-GT",
Language.esHN: cstring"es-HN",
Language.esMX: cstring"es-MX",
Language.esNI: cstring"es-NI",
Language.esPA: cstring"es-PA",
Language.esPE: cstring"es-PE",
Language.esPR: cstring"es-PR",
Language.esPY: cstring"es-PY",
Language.esSV: cstring"es-SV",
Language.esUS: cstring"es-US",
Language.esUY: cstring"es-UY",
Language.esVE: cstring"es-VE",
Language.etEE: cstring"et-EE",
Language.euES: cstring"eu-ES",
Language.faIR: cstring"fa-IR",
Language.fiFI: cstring"fi-FI",
Language.filPH: cstring"fil-PH",
Language.foFO: cstring"fo-FO",
Language.frBE: cstring"fr-BE",
Language.frCA: cstring"fr-CA",
Language.frCH: cstring"fr-CH",
Language.frFR: cstring"fr-FR",
Language.frLU: cstring"fr-LU",
Language.frMC: cstring"fr-MC",
Language.fyNL: cstring"fy-NL",
Language.gaIE: cstring"ga-IE",
Language.gdGB: cstring"gd-GB",
Language.glES: cstring"gl-ES",
Language.gswFR: cstring"gsw-FR",
Language.guIN: cstring"gu-IN",
Language.haNG: cstring"ha-NG",
Language.heIL: cstring"he-IL",
Language.hiIN: cstring"hi-IN",
Language.hrBA: cstring"hr-BA",
Language.hrHR: cstring"hr-HR",
Language.hsbDE: cstring"hsb-DE",
Language.huHU: cstring"hu-HU",
Language.hyAM: cstring"hy-AM",
Language.idID: cstring"id-ID",
Language.igNG: cstring"ig-NG",
Language.iiCN: cstring"ii-CN",
Language.isIS: cstring"is-IS",
Language.itCH: cstring"it-CH",
Language.itIT: cstring"it-IT",
Language.iuCA: cstring"iu-CA",
Language.jaJP: cstring"ja-JP",
Language.kaGE: cstring"ka-GE",
Language.kkKZ: cstring"kk-KZ",
Language.klGL: cstring"kl-GL",
Language.kmKH: cstring"km-KH",
Language.knIN: cstring"kn-IN",
Language.koKR: cstring"ko-KR",
Language.kokIN: cstring"kok-IN",
Language.kyKG: cstring"ky-KG",
Language.lbLU: cstring"lb-LU",
Language.loLA: cstring"lo-LA",
Language.ltLT: cstring"lt-LT",
Language.lvLV: cstring"lv-LV",
Language.miNZ: cstring"mi-NZ",
Language.mkMK: cstring"mk-MK",
Language.mlIN: cstring"ml-IN",
Language.mnCN: cstring"mn-CN",
Language.mnMN: cstring"mn-MN",
Language.mohCA: cstring"moh-CA",
Language.mrIN: cstring"mr-IN",
Language.msBN: cstring"ms-BN",
Language.msMY: cstring"ms-MY",
Language.mtMT: cstring"mt-MT",
Language.nbNO: cstring"nb-NO",
Language.neNP: cstring"ne-NP",
Language.nlBE: cstring"nl-BE",
Language.nlNL: cstring"nl-NL",
Language.nnNO: cstring"nn-NO",
Language.nsoZA: cstring"nso-ZA",
Language.ocFR: cstring"oc-FR",
Language.orIN: cstring"or-IN",
Language.paIN: cstring"pa-IN",
Language.plPL: cstring"pl-PL",
Language.prsAF: cstring"prs-AF",
Language.psAF: cstring"ps-AF",
Language.ptBR: cstring"pt-BR",
Language.ptPT: cstring"pt-PT",
Language.qutGT: cstring"qut-GT",
Language.quzBO: cstring"quz-BO",
Language.quzEC: cstring"quz-EC",
Language.quzPE: cstring"quz-PE",
Language.rmCH: cstring"rm-CH",
Language.roRO: cstring"ro-RO",
Language.ruRU: cstring"ru-RU",
Language.rwRW: cstring"rw-RW",
Language.saIN: cstring"sa-IN",
Language.sahRU: cstring"sah-RU",
Language.seFI: cstring"se-FI",
Language.seNO: cstring"se-NO",
Language.seSE: cstring"se-SE",
Language.siLK: cstring"si-LK",
Language.skSK: cstring"sk-SK",
Language.slSI: cstring"sl-SI",
Language.smaNO: cstring"sma-NO",
Language.smaSE: cstring"sma-SE",
Language.smjNO: cstring"smj-NO",
Language.smjSE: cstring"smj-SE",
Language.smnFI: cstring"smn-FI",
Language.smsFI: cstring"sms-FI",
Language.sqAL: cstring"sq-AL",
Language.srBA: cstring"sr-BA",
Language.srCS: cstring"sr-CS",
Language.srME: cstring"sr-ME",
Language.srRS: cstring"sr-RS",
Language.svFI: cstring"sv-FI",
Language.svSE: cstring"sv-SE",
Language.swKE: cstring"sw-KE",
Language.syrSY: cstring"syr-SY",
Language.taIN: cstring"ta-IN",
Language.teIN: cstring"te-IN",
Language.tgTJ: cstring"tg-TJ",
Language.thTH: cstring"th-TH",
Language.tkTM: cstring"tk-TM",
Language.tnZA: cstring"tn-ZA",
Language.trTR: cstring"tr-TR",
Language.ttRU: cstring"tt-RU",
Language.tzmDZ: cstring"tzm-DZ",
Language.ugCN: cstring"ug-CN",
Language.ukUA: cstring"uk-UA",
Language.urPK: cstring"ur-PK",
Language.uzUZ: cstring"uz-UZ",
Language.viVN: cstring"vi-VN",
Language.woSN: cstring"wo-SN",
Language.xhZA: cstring"xh-ZA",
Language.yoNG: cstring"yo-NG",
Language.zhCN: cstring"zh-CN",
Language.zhHK: cstring"zh-HK",
Language.zhMO: cstring"zh-MO",
Language.zhSG: cstring"zh-SG",
Language.zhTW: cstring"zh-TW",
Language.zuZA: cstring"zu-ZA"]

10
karax/localstorage.nim Normal file
View file

@ -0,0 +1,10 @@
## Karax -- Single page applications for Nim.
## This module contains wrappers for the HTML 5 local storage.
proc getItem*(key: cstring): cstring {.importc: "localStorage.getItem".}
proc setItem*(key, value: cstring) {.importc: "localStorage.setItem".}
proc hasItem*(key: cstring): bool {.importcpp: "(localStorage.getItem(#) !== null)".}
proc clear*() {.importc: "localStorage.clear".}
proc removeItem*(key: cstring) {.importc: "localStorage.removeItem".}

38
karax/lookuptables.nim Normal file
View file

@ -0,0 +1,38 @@
import macros, kbase
from strutils import `%`
macro fieldNamesAsArray*(t: typed; pattern = "$1"): untyped =
var impl = getTypeImpl(getTypeImpl(t)[1])
if impl.kind == nnkRefTy: impl = getTypeImpl(impl[0])
expectKind impl, nnkObjectTy
impl = impl[2]
expectKind impl, nnkRecList
result = newTree(nnkBracket)
for x in impl:
expectKind x, nnkIdentDefs
result.add(newCall(bindSym"kstring", newLit(pattern.strVal % $x[0])))
proc str(n: NimNode): NimNode =
if n.kind in {nnkStrLit, nnkTripleStrLit}:
result = newCall(bindSym"kstring", n)
else:
result = copyNimNode(n)
for i in 0..<n.len:
result.add str(n[i])
macro kstrLits*(x: untyped): untyped =
## Transforms every string literal "abc" to ``kstring"abc"``. This makes
## things much easier to write. String literals of the form ``r"abc"`` are
## not affected.
result = str(x)
when isMainModule:
type
MyObject = ref object
x*: string
next*: int
more, here*: float
const myObjectFields = fieldNamesAsArray(MyObject, "MyObject_$1")
for f in myObjectFields: echo f

3
karax/prelude.nim Normal file
View file

@ -0,0 +1,3 @@
## Include file that contains the common imports for the Karax framework.
import kbase, karax, karaxdsl, vdom, compact, jstrutils

380
karax/reactive.nim Normal file
View file

@ -0,0 +1,380 @@
import jdict
type
Message = enum
Unchanged
Changed
Mark
Inserted
Deleted
Replaced
State = object
stale: int
outdated: bool
phantom: bool
type
SinkSeq = seq[proc(msg: Message, pos: int)]
ReactiveBase* = ref object of RootObj ## everything that is a "reactive"
## value derives from that
sinks*: SinkSeq
dups: JDict[cstring, int]
id: int
Reactive*[T] = ref object of ReactiveBase
value*: T
RString* = Reactive[cstring]
# RTime* = Reactive[Time]
RInt* = Reactive[int]
RBool* = Reactive[bool]
RSeq*[T] = ref object of ReactiveBase
s: seq[T]
L: RInt
var rid: int
proc addSink(x: ReactiveBase; key: cstring; sink: proc(msg: Message; pos: int)) =
if x.dups == nil: x.dups = newJDict[cstring, int]()
if not x.dups.contains(key):
x.dups[key] = x.sinks.len
x.sinks.add sink
if x.id == 0:
inc rid
x.id = rid
else:
# update existing entry:
x.sinks[x.dups[key]] = sink
proc addSink(x: ReactiveBase; sink: proc(msg: Message; pos: int)) =
x.sinks.add sink
if x.id == 0:
inc rid
x.id = rid
var inhibited: int
proc broadcast(x: ReactiveBase, msg: Message; pos = 0) =
if inhibited == 0:
for s in x.sinks: s(msg, pos)
var toTrack: seq[(cstring, proc (msg: Message; pos: int))] = @[]
template withTrack(key, t, body) =
toTrack.add((key, t))
body
discard toTrack.pop()
proc trackDependency*(r: ReactiveBase) =
for t in toTrack:
r.addSink t[0], t[1]
template wrapObserver(f: untyped) =
var state: State
proc helper(msg: Message; pos: int) =
case msg.kind:
of Changed, Unchanged:
state.outdated = state.outdated or (msg.kind == Changed)
dec state.stale
if state.stale == 0:
if state.outdated:
var t = f()
let thisChanged = if x.value != t: Changed else: Unchanged
x.value = t
x.broadcast(thisChanged, pos)
state.outdated = false
else:
x.broadcast(Unchanged, pos)
of Mark:
if state.stale == 0: x.broadcast(msg, pos)
inc state.stale
of Inserted, Deleted, Replaced:
dec state.stale
if state.stale == 0:
x.broadcast(msg, pos)
#state.phantom = true
helper
template glitchFree(f: untyped) =
var state: State
proc helper(msg: Message; pos: int) =
case msg.kind:
of Changed, Unchanged:
state.outdated = state.outdated or (msg.kind == Changed)
dec state.stale
if state.stale == 0:
if state.outdated:
x.value = f
x.broadcast(msg.kind, pos)
state.outdated = false
else:
x.broadcast(Unchanged, pos)
of Mark:
if state.stale == 0: x.broadcast(msg, pos)
inc state.stale
of Inserted, Deleted, Replaced:
dec state.stale
if state.stale == 0:
x.broadcast(msg, pos)
helper
proc `<-`*[T](x: Reactive[T], val: T) =
#if x.value != val:
x.value = val
x.broadcast(Mark)
x.broadcast(Changed)
proc notifyObservers*(x: ReactiveBase) =
x.broadcast(Mark)
x.broadcast(Changed)
proc subscribe*[T](x: Reactive[T], f: proc(x: T)) =
let reactor = proc (msg: Message, pos: int) =
case msg:
of Mark: discard
of Changed, Unchanged: f(x.value)
of Inserted, Deleted, Replaced: discard
x.addSink reactor
proc subscribeSelf*[T: ReactiveBase](x: T, f: proc()) =
let reactor = proc (msg: Message, pos: int) =
case msg:
of Mark: discard
of Changed, Unchanged: f()
of Inserted, Deleted, Replaced: discard
x.addSink reactor
template lift1(op: untyped) =
proc op[T](a: Reactive[T]): (proc(): T) =
result = proc(): T = op(a.now)
template lift2(op: untyped) =
proc op[T](a, b: Reactive[T]): (proc(): T) =
result = proc(): T = op(a.now, b.now)
lift1 `not`
lift2 `&`
proc newReactive*[T](x: T): Reactive[T] =
result = Reactive[T](value: x)
proc rstr*(x: cstring): RString =
result = RString(value: x)
proc newRSeq*[T](len: int = 0): RSeq[T] =
result = RSeq[T](s: newSeq[T](len), L: newReactive[int](0))
proc newRSeq*[T](data: seq[T]): RSeq[T] =
result = RSeq[T](s: newSeq[T](data.len), L: newReactive[int](0))
for i in 0..high(data):
result.s[i] = data[i]
proc `[]=`*[T](x: RSeq[T]; index: int; v: T) =
x.s[index] = v
x.broadcast(Mark)
x.broadcast(Replaced, index)
proc `[]`*[T](x: RSeq[T]; index: int): T = x.s[index]
proc len*[T](x: RSeq[T]): int = x.s.len
proc add*[T](x: RSeq[T]; y: T) =
let position = x.s.len
x.s.add(y)
x.broadcast(Mark)
x.broadcast(Inserted, position)
proc insert*[T](x: RSeq[T]; y: T; position = 0) =
x.s.insert(y, position)
x.broadcast(Mark)
x.broadcast(Inserted, position)
proc delete*[T](x: RSeq[T]; position = 0) =
x.s.delete(position)
x.broadcast(Mark)
x.broadcast(Deleted, position)
proc deleteElem*[T](x: RSeq[T]; y: T) =
var position = -1
for i in 0..<x.len:
if x[i] == y:
position = i; break
if position >= 0:
x.s.delete(position)
x.broadcast(Mark)
x.broadcast(Deleted, position)
when false:
proc `:=`[T](x: Reactive[T], f: proc(): T) =
toTrack = wrapObserver(f())
x.value = f()
toTrack = nil
proc map*[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] =
let xl = x.L.value
let res = newRSeq[U](xl)
for i in 0..<xl:
res.s[i] := proc(): U = f(x[i].now)
let reactor = proc (msg: Message, pos: int) =
case msg:
of Mark, Changed, Unchanged: discard "nothing to do"
of Inserted:
res.insert(f(x[pos]), pos)
of Deleted:
res.delete(pos)
x.addSink reactor
result = res
import macros
template protect(body: untyped) =
#var tmp: seq[proc(msg: Message, pos: int)]
#swap(r.sinks, tmp)
inc inhibited
body
dec inhibited
#swap(r.sinks, tmp)
template trackStart(key: cstring; a, b: untyped) =
proc differ(m: Message; pos: int) =
if m == Changed:
protect:
karax.runDiff(kxi, a, b)
toTrack.add((key, differ))
template trackEnd() =
discard toTrack.pop()
template doTrack(r: ReactiveBase; a, b: untyped) {.dirty.} =
bind addSink, Message, RSeq, Changed, Deleted, Inserted
addSink r, proc(m: Message; pos: int) =
#when r is RSeq:
#echo "Message: ", m, " ", pos
if m == Changed:
protect r:
karax.runDiff(kxi, a, b)
template doTrackResize(r: ReactiveBase; a, b: untyped) {.dirty.} =
bind addSink, Message, RSeq, Changed, Deleted, Inserted
addSink r, proc(m: Message; pos: int) =
#when r is RSeq:
#echo "Message: ", m, " ", pos
case m
of Deleted: karax.runDel(kxi, a, pos)
of Inserted:
let it {.used.} = r[pos]
karax.runIns(kxi, a, b, pos)
else: discard
macro track*(procDef: untyped): untyped =
let params = params(procDef)
var trackings = newStmtList()
var inner = copyNimTree(procDef)
inner[0] = ident($procDef.name & "Inner")
var call = newCall(inner[0])
for j in 1..<params.len:
let x = params[j]
expectKind x, nnkIdentDefs
for i in 0..x.len-3:
let param = x[i]
call.add(param)
trackings.add inner
let key = newCall("cstring", newLit lineInfo(procDef))
trackings.add getAst(trackStart(key, ident"result", call))
trackings.add newAssignment(ident"result", call)
trackings.add getAst(trackEnd())
result = copyNimTree(procDef)
result.body = trackings
when defined(debugKaraxDsl):
echo repr result
proc generatePrivateAccessors(name, hidden, typ, fieldTyp: NimNode): NimNode =
template helper(name, hidden, typ, fieldTyp) {.dirty.} =
proc name(self: typ): fieldTyp =
trackDependency(self)
result = self.hidden
proc `name=`(self: typ; val: fieldTyp) =
self.hidden = val
notifyObservers(self)
result = getAst(helper(name, hidden, typ, fieldTyp))
proc generatePublicAccessors(name, hidden, typ, fieldTyp: NimNode): NimNode =
template helper(name, hidden, typ, fieldTyp) {.dirty.} =
proc name*(self: typ): fieldTyp =
trackDependency(self)
result = self.hidden
proc `name=`*(self: typ; val: fieldTyp) =
self.hidden = val
notifyObservers(self)
result = getAst(helper(name, hidden, typ, fieldTyp))
proc transform(n: NimNode; stmts, obj: NimNode): NimNode =
if n.kind == nnkIdentDefs and obj.kind == nnkIdent:
for i in 0..n.len-3:
let it = n[i]
let itB = if it.kind == nnkPostFix: it[1] else: it
let hidden = newIdentNode("raw" & $itB)
if it.kind == nnkPostFix:
stmts.add generatePublicAccessors(itB, hidden, obj, n[n.len-2])
else:
stmts.add generatePrivateAccessors(itB, hidden, obj, n[n.len-2])
n[i] = hidden
result = n
else:
var objB = if n.kind == nnkTypeDef: n[0] else: obj
result = copyNimNode(n)
for i in 0..<n.len:
if n.kind == nnkObjectTy and i == 1 and n[1].kind == nnkEmpty:
result.add newTree(nnkOfInherit, ident("ReactiveBase"))
else:
result.add transform(n[i], stmts, objB)
macro makeReactive*(n: untyped): untyped =
var a = newStmtList()
a.add newEmptyNode()
let t = transform(n, a, newEmptyNode())
a[0] = t
result = a
when defined(debugKaraxDsl):
echo repr result
import vdom
template vmap*(x: RSeq; elem, f: untyped): VNode =
let tmp = buildHtml(elem):
for i in 0..<len(x):
f(x[i])
doTrackResize(x, tmp, f(x[pos]))
tmp
template vmapIt*(x: RSeq; elem, call: untyped): VNode =
var it {.inject.}: type(x[0])
let tmp = buildHtml(elem):
for i in 0..<len(x):
it = x[i]
call
doTrackResize(x, tmp, call)
tmp
proc text*(s: RString): VNode =
result = text(s.value)
s.subscribe proc(v: cstring) =
if result.dom != nil: result.dom.nodeValue = v
when isMainModule:
makeReactive:
type
Foo = ref object of ReactiveBase
x, y: int
Bar = ref object of ReactiveBase
z: string
exported*: int
Another = ref object
a, b: cstring
c: bool

129
karax/tools/karun.nim Normal file
View file

@ -0,0 +1,129 @@
import os, strutils, browsers,times, tables
import parseopt
import threadpool
import static_server
const
css = """
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
"""
const html = """
<!DOCTYPE html>
<html>
<head>
<meta content="width=device-width, initial-scale=1" name="viewport" />
<title>$1</title>
$2
</head>
<body id="body" class="site">
<div id="ROOT"></div>
<script type="text/javascript" src="/app.js"></script>
$3
</body>
</html>
"""
const websocket = """
<script type="text/javascript">
var ws = new WebSocket("ws://localhost:8080/ws");
ws.onopen = function(evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
ws.onmessage = function(evt) {
console.log( "Received Message: " + evt.data);
if(evt.data == "refresh"){
window.location.href = window.location.href
}
};
ws.onclose = function(evt) {
console.log("Connection closed.");
};
</script>
"""
proc exec(cmd: string) =
if os.execShellCmd(cmd) != 0:
quit "External command failed: " & cmd
proc build(rest: string, selectedCss: string, run: bool, watch: bool) =
echo("Building...")
let cmd = "nim js --out:" & "app" & ".js " & rest
if watch:
discard os.execShellCmd(cmd)
else:
exec cmd
let dest = "app" & ".html"
let script = if watch: websocket else: ""
writeFile(dest, html % ["app", selectedCss, script])
if run: openDefaultBrowser("http://localhost:8080")
proc watchBuild(filePath: string, selectedCss: string, rest: string) {.thread.} =
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
while true:
sleep(300)
for path in walkDirRec("."):
if ".git" in path:
continue
var (_, _, ext) = splitFile(path)
if ext in [".scss",".sass",".less",".styl",".pcss",".postcss"]:
continue
if files.hasKey(path):
if files[path] != getLastModificationTime(path):
echo("File changed: " & path)
build(rest,selectedCss, false, true)
files[path] = getLastModificationTime(path)
else:
if absolutePath(path) in [absolutePath("app" & ".js"),absolutePath("app" & ".html")]:
continue
files[path] = getLastModificationTime(path)
proc serve(){.thread.} =
serveStatic()
proc main =
var op = initOptParser()
var rest = op.cmdLineRest
var file = ""
var run = false
var watch = false
var selectedCss = ""
while true:
op.next()
case op.kind
of cmdLongOption:
case op.key
of "run":
run = true
rest = rest.replace("--run ")
of "css":
if op.val != "":
selectedCss = readFile(op.val)
else:
selectedCss = css
rest = rest.substr(rest.find(" "))
else: discard
of cmdShortOption:
if op.key == "r":
run = true
rest = rest.replace("-r ")
if op.key == "w":
watch = true
rest = rest.replace("-w ")
of cmdArgument: file = op.key
of cmdEnd: break
if file.len == 0: quit "filename expected"
if run:
spawn serve()
if watch:
spawn watchBuild(file, selectedCss, rest)
build(rest,selectedCss, run, watch)
sync()
main()

1
karax/tools/karun.nims Normal file
View file

@ -0,0 +1 @@
switch("threads", "on")

View file

@ -0,0 +1,141 @@
import
std/[net, os, strutils, uri, mimetypes, asyncnet, asyncdispatch, md5,
logging, httpcore, asyncfile, asynchttpserver, tables, times]
from cgi import decodeUrl
import ws, dotenv
var logger = newConsoleLogger()
addHandler(logger)
when defined(release):
setLogFilter(lvlError)
type
RawHeaders* = seq[tuple[key, val: string]]
proc toStr(headers: RawHeaders): string =
$newHttpHeaders(headers)
proc send(request: Request, code: HttpCode, headers: RawHeaders,
body: string): Future[void] =
return request.respond(code, body, newHttpHeaders(headers))
proc statusContent(request: Request, status: HttpCode, content: string,
headers: RawHeaders): Future[void] =
try:
result = send(request, status, headers, content)
debug(" ", status, " ", toStr(headers))
except:
error("Could not send response: ", osErrorMsg(osLastError()))
proc sendStaticIfExists(req: Request, paths: seq[string]): Future[HttpCode] {.async.} =
result = Http200
let mimes = newMimetypes()
for p in paths:
if fileExists(p):
if fpOthersRead notin getFilePermissions(p):
return Http403
let fileSize = getFileSize(p)
let extPos = searchExtPos(p)
let mimetype = mimes.getMimetype(
if extPos >= 0: p.substr(extPos + 1)
else: "")
if fileSize < 10_000_000: # 10 mb
var file = readFile(p)
var hashed = getMD5(file)
# If the user has a cached version of this file and it matches our
# version, let them use it
if req.headers.getOrDefault("If-None-Match") == hashed:
await req.statusContent(Http304, "", default(RawHeaders))
else:
await req.statusContent(Http200, file, @{
"Content-Type": mimetype,
"ETag": hashed
})
else:
let headers = @{
"Content-Type": mimetype,
"Content-Length": $fileSize
}
await req.statusContent(Http200, "", headers)
var fileStream = newFutureStream[string]("sendStaticIfExists")
var file = openAsync(p, fmRead)
# Let `readToStream` write file data into fileStream in the
# background.
asyncCheck file.readToStream(fileStream)
# The `writeFromStream` proc will complete once all the data in the
# `bodyStream` has been written to the file.
while true:
let (hasValue, value) = await fileStream.read()
if hasValue:
await req.client.send(value)
else:
break
file.close()
return
# If we get to here then no match could be found.
return Http404
proc handleFileRequest(req: Request): Future[HttpCode] {.async.} =
# Find static file.
var reqPath = cgi.decodeUrl(req.url.path)
var staticDir = getEnv("staticDir") # it's assumed a relative dir
var status = Http400
var path = staticDir / reqPath
normalizePathEnd(path, false)
if dirExists(path):
status = await sendStaticIfExists(req, @[path / "index.html", path / "index.htm"])
else:
status = await sendStaticIfExists(req, @[path])
return status
proc handleWs(req: Request) {.async.} =
var ws = await newWebSocket(req)
await ws.send("Welcome to simple echo server")
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
let watchedFiles = [absolutePath "app.js", absolutePath "app.html"]
for path in watchedFiles:
files[path] = getLastModificationTime(path)
while ws.readyState == Open:
await sleepAsync(500)
var changed = false
for path in watchedFiles:
if files[path] != getLastModificationTime(path):
changed = true
files[path] = getLastModificationTime(path)
if changed:
await ws.send("refresh")
changed = false
proc serveStatic*() =
if fileExists("static.env"):
var env: DotEnv
env = initDotEnv(getCurrentDir(), "static.env")
env.overload()
else:
putEnv("staticDir", "assets/")
var server = newAsyncHttpServer()
proc cb(req: Request) {.gcsafe, async.} =
if req.url.path == "/ws":
await handleWs(req)
if req.url.path == "/":
await req.respond(Http200, readFile "app.html")
elif req.url.path == "/app.js":
let file = absolutePath("app" & ".js")
if not file.fileExists:
error(file, " does not exist!")
if fpUserRead notin os.getFilePermissions(file):
error("Could not read ", file, "!")
await req.respond(Http200, readFile(file))
else:
let status = await handleFileRequest(req)
if status != Http200:
await req.respond(status, "")
waitFor server.serve(Port(8080), cb)
when isMainModule:
serveStatic()

490
karax/vdom.nim Normal file
View file

@ -0,0 +1,490 @@
## Virtual DOM implementation for Karax.
when defined(js):
from kdom import Event, Node
else:
type
Event* = ref object
Node* = ref object
import macros, vstyles, kbase
from strutils import toUpperAscii, toLowerAscii, tokenize
type
VNodeKind* {.pure.} = enum
text = "#text", int = "#int", bool = "#bool",
vthunk = "#vthunk", dthunk = "#dthunk",
component = "#component", verbatim = "#verbatim",
html, head, title, base, link, meta, style,
script, noscript,
body, section, nav, article, aside,
h1, h2, h3, h4, h5, h6,
header, footer, address, main,
p, hr, pre, blockquote, ol, ul, li,
dl, dt, dd,
figure, figcaption,
tdiv = "div",
a, em, strong, small,
strikethrough = "s", cite, quote,
dfn, abbr, data, time, code, `var` = "var", samp,
kdb, sub, sup, italic = "i", bold = "b", underlined = "u",
mark, ruby, rt, rp, bdi, dbo, span, br, wbr,
ins, del, img, iframe, embed, `object` = "object",
param, video, audio, source, track, canvas, map,
area, math,
# SVG elements, see: https://www.w3.org/TR/SVG2/eltindex.html
animate, animateMotion, animateTransform, circle, clipPath, defs, desc,
`discard` = "discard", ellipse, feBlend, feColorMatrix, feComponentTransfer,
feComposite, feConvolveMatrix, feDiffuseLighting, feDisplacementMap,
feDistantLight, feDropShadow, feFlood, feFuncA, feFuncB, feFuncG, feFuncR,
feGaussianBlur, feImage, feMerge, feMergeNode, feMorphology, feOffset,
fePointLight, feSpecularLighting, feSpotLight, feTile, feTurbulence,
filter, foreignObject, g, image, line, linearGradient, marker, mask,
metadata, mpath, path, pattern, polygon, polyline, radialGradient, rect,
`set` = "set", stop, svg, switch, symbol, txt = "text", textPath, tspan,
unknown, use, view,
table, caption, colgroup, col, tbody, thead,
tfoot, tr, td, th,
form, fieldset, legend, label, input, button,
select, datalist, optgroup, option, textarea,
keygen, output, progress, meter,
details, summary, command, menu
const selfClosing = {area, base, br, col, embed, hr, img, input,
link, meta, param, source, track, wbr}
type
EventKind* {.pure.} = enum ## The events supported by the virtual DOM.
onclick, ## An element is clicked.
oncontextmenu, ## An element is right-clicked.
ondblclick, ## An element is double clicked.
onkeyup, ## A key was released.
onkeydown, ## A key is pressed.
onkeypressed, # A key was pressed.
onfocus, ## An element got the focus.
onblur, ## An element lost the focus.
onchange, ## The selected value of an element was changed.
onscroll, ## The user scrolled within an element.
onmousedown, ## A pointing device button (usually a mouse) is pressed
## on an element.
onmouseenter, ## A pointing device is moved onto the element that
## has the listener attached.
onmouseleave, ## A pointing device is moved off the element that
## has the listener attached.
onmousemove, ## A pointing device is moved over an element.
onmouseout, ## A pointing device is moved off the element that
## has the listener attached or off one of its children.
onmouseover, ## A pointing device is moved onto the element that has
## the listener attached or onto one of its children.
onmouseup, ## A pointing device button is released over an element.
ondrag, ## An element or text selection is being dragged (every 350ms).
ondragend, ## A drag operation is being ended (by releasing a mouse button
## or hitting the escape key).
ondragenter, ## A dragged element or text selection enters a valid drop target.
ondragleave, ## A dragged element or text selection leaves a valid drop target.
ondragover, ## An element or text selection is being dragged over a valid
## drop target (every 350ms).
ondragstart, ## The user starts dragging an element or text selection.
ondrop, ## An element is dropped on a valid drop target.
onsubmit, ## A form is submitted
oninput, ## An input value changes
onanimationstart,
onanimationend,
onanimationiteration,
onkeyupenter, ## vdom extension: an input field received the ENTER key press
onkeyuplater, ## vdom extension: a key was pressed and some time
## passed (useful for on-the-fly text completions)
onload, # img
ontransitioncancel,
ontransitionend,
ontransitionrun,
ontransitionstart,
onwheel # fires when the user rotates a wheel button on a pointing device.
macro buildLookupTables(): untyped =
var a = newTree(nnkBracket)
for i in low(VNodeKind)..high(VNodeKind):
let x = $i
let y = if x[0] == '#': x else: toUpperAscii(x)
a.add(newCall("kstring", newLit(y)))
var e = newTree(nnkBracket)
for i in low(EventKind)..high(EventKind):
e.add(newCall("kstring", newLit(substr($i, 2))))
template tmpl(a, e) {.dirty.} =
const
toTag*: array[VNodeKind, kstring] = a
toEventName*: array[EventKind, kstring] = e
result = getAst tmpl(a, e)
buildLookupTables()
type
EventHandler* = proc (ev: Event; target: VNode) {.closure.}
NativeEventHandler* = proc (ev: Event) {.closure.}
EventHandlers* = seq[(EventKind, EventHandler, NativeEventHandler)]
VKey* = kstring
VNode* = ref object of RootObj
kind*: VNodeKind
index*: int ## a generally useful 'index'
id*, class*, text*: kstring
kids: seq[VNode]
# even index: key, odd index: value; done this way for memory efficiency:
attrs: seq[kstring]
events*: EventHandlers
when false:
hash*: Hash
validHash*: bool
style*: VStyle ## the style that should be applied to the virtual node.
dom*: Node ## the attached real DOM node. Can be 'nil' if the virtual node
## is not part of the virtual DOM anymore.
VComponent* = ref object of VNode ## The abstract class for every karax component.
key*: VKey ## key that determines if two components are
## identical.
renderImpl*: proc(self: VComponent): VNode
changedImpl*: proc(self, newInstance: VComponent): bool
updatedImpl*: proc(self, newInstance: VComponent)
onAttachImpl*: proc(self: VComponent)
onDetachImpl*: proc(self: VComponent)
version*: int ## Update this to trigger a redraw by karax. Usually you
## should call 'markDirty' instead which is an alias for
## 'inc version'.
renderedVersion*: int ## Do not touch. Used by karax. The last version of the
## component we rendered.
expanded*: VNode ## Do not touch. Used by karax. The VDOM the component
## expanded to.
debugId*: int
proc value*(n: VNode): kstring = n.text
proc `value=`*(n: VNode; v: kstring) = n.text = v
proc intValue*(n: VNode): int = n.index
proc vn*(i: int): VNode = VNode(kind: VNodeKind.int, index: i)
proc vn*(b: bool): VNode = VNode(kind: VNodeKind.int, index: ord(b))
proc vn*(x: kstring): VNode = VNode(kind: VNodeKind.text, index: -1, text: x)
template callThunk*(fn: typed; n: VNode): untyped =
## for internal usage only.
fn(n.kids)
proc vthunk*(name: kstring; args: varargs[VNode, vn]): VNode =
VNode(kind: VNodeKind.vthunk, text: name, index: -1, kids: @args)
proc dthunk*(dom: Node): VNode =
VNode(kind: VNodeKind.dthunk, dom: dom)
proc setEventIfNoConflict(v: VNode; kind: EventKind; handler: EventHandler) =
assert handler != nil
for i in 0..<v.events.len:
if v.events[i][0] == kind:
#v.events[i][1] = handler
return
v.events.add((kind, handler, nil))
proc mergeEvents*(v: VNode; handlers: EventHandlers) =
## Overrides or adds the event handlers to `v`'s internal event handler list.
for h in handlers: v.setEventIfNoConflict(h[0], h[1])
proc defaultChangedImpl*(v, newInstance: VComponent): bool =
## The default implementation of 'changed'.
result = v.key != newInstance.key or v.version != v.renderedVersion
proc defaultUpdatedImpl*(v, newInstance: VComponent) =
discard
var gid = 0
proc getDebugId(): int =
inc(gid)
gid
template newComponent*[T](t: typeDesc[T];
render: (proc(self: VComponent): VNode) = nil,
onAttach: proc(self: VComponent) = nil,
onDetach: proc(self: VComponent) = nil,
changed: (proc(self, newInstance: VComponent): bool) = defaultChangedImpl,
updated: proc(self, newInstance: VComponent) = defaultUpdatedImpl): T =
## Use this template to create new components.
T(kind: VNodeKind.component, index: -1,
text: kstring(astToStr(t)), renderImpl: render,
changedImpl: changed, updatedImpl: updated,
onAttachImpl: onAttach, onDetachImpl: onDetach,
debugId: getDebugId())
template markDirty*(c: VComponent) =
## mark the component as dirty so that it is re-rendered.
inc c.version
proc setAttr*(n: VNode; key: kstring; val: kstring = "") =
if n.attrs.len == 0:
n.attrs = @[key, val]
else:
for i in countup(0, n.attrs.len-2, 2):
if n.attrs[i] == key:
n.attrs[i+1] = val
return
n.attrs.add key
n.attrs.add val
proc getAttr*(n: VNode; key: kstring): kstring =
for i in countup(0, n.attrs.len-2, 2):
if n.attrs[i] == key: return n.attrs[i+1]
proc takeOverAttr*(newNode, oldNode: VNode) =
shallowCopy oldNode.attrs, newNode.attrs
proc takeOverFields*(newNode, oldNode: VNode) =
template take(field) =
shallowCopy oldNode.field, newNode.field
take kind
take index
take id
take class
take text
take kids
take attrs
take events
take style
take dom
proc len*(x: VNode): int = x.kids.len
proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx]
proc `[]=`*(x: VNode; idx: int; y: VNode) = x.kids[idx] = y
proc add*(parent, kid: VNode) =
when not defined(js) and not defined(nimNoNil):
if parent.kids.isNil: parent.kids = @[]
parent.kids.add kid
proc delete*(parent: VNode; position: int) =
parent.kids.delete(position)
proc insert*(parent, kid: VNode; position: int) =
parent.kids.insert(kid, position)
proc newVNode*(kind: VNodeKind): VNode = VNode(kind: kind, index: -1)
proc tree*(kind: VNodeKind; kids: varargs[VNode]): VNode =
result = newVNode(kind)
for k in kids: result.add k
proc tree*(kind: VNodeKind; attrs: openarray[(kstring, kstring)];
kids: varargs[VNode]): VNode =
result = tree(kind, kids)
for a in attrs: result.setAttr(a[0], a[1])
when defined(js):
proc text*(s: string): VNode = VNode(kind: VNodeKind.text, text: kstring(s), index: -1)
proc text*(s: kstring): VNode = VNode(kind: VNodeKind.text, text: s, index: -1)
when defined(js):
proc verbatim*(s: string): VNode =
VNode(kind: VNodeKind.verbatim, text: kstring(s), index: -1)
proc verbatim*(s: kstring): VNode =
VNode(kind: VNodeKind.verbatim, text: s, index: -1)
iterator items*(n: VNode): VNode =
for i in 0..<n.kids.len: yield n.kids[i]
iterator attrs*(n: VNode): (kstring, kstring) =
for i in countup(0, n.attrs.len-2, 2):
yield (n.attrs[i], n.attrs[i+1])
proc sameAttrs*(a, b: VNode): bool =
if a.attrs.len == b.attrs.len:
result = true
for i in 0 ..< a.attrs.len:
if a.attrs[i] != b.attrs[i]: return false
proc addEventListener*(n: VNode; event: EventKind; handler: EventHandler) =
n.events.add((event, handler, nil))
when kstring is cstring:
proc len(a: kstring): int =
# xxx: maybe move where kstring is defined
# without this, `n.field.len` fails on js (non web) platform
if a == nil: 0 else: a.len
template toStringAttr(field) =
if n.field.len > 0:
result.add " " & astToStr(field) & " = " & $n.field
proc toString*(n: VNode; result: var string; indent: int) =
for i in 1..indent: result.add ' '
if result.len > 0: result.add '\L'
result.add "<" & $n.kind
toStringAttr(id)
toStringAttr(class)
for k, v in attrs(n):
result.add " " & $k & " = " & $v
result.add ">\L"
if n.kind == VNodeKind.text:
result.add n.text
else:
if n.text.len > 0:
result.add " value = "
result.add n.text
for child in items(n):
toString(child, result, indent+2)
for i in 1..indent: result.add ' '
result.add "\L</" & $n.kind & ">"
when false:
proc calcHash*(n: VNode) =
if n.validHash: return
n.validHash = true
var h: Hash = ord n.kind
if n.id != nil:
h &= "id"
h &= n.id
if n.class != nil:
h &= "class"
h &= n.class
if n.key >= 0:
h &= "k"
h &= n.key
for k, v in attrs(n):
h &= " "
h &= k
h &= "="
h &= v
if n.kind == VNodeKind.text or n.text != nil:
h &= "t"
h &= n.text
else:
for child in items(n):
calcHash(child)
h &= child.hash
n.hash = h
proc add*(result: var string, n: VNode, indent = 0, indWidth = 2) =
## adds the textual representation of `n` to `result`.
proc addEscapedAttr(result: var string, s: kstring) =
# `addEscaped` alternative with less escaped characters.
# Only to be used for escaping attribute values enclosed in double quotes!
for c in items(s):
case c
of '<': result.add("&lt;")
of '>': result.add("&gt;")
of '&': result.add("&amp;")
of '"': result.add("&quot;")
else: result.add(c)
proc addEscaped(result: var string, s: kstring) =
## same as ``result.add(escape(s))``, but more efficient.
for c in items(s):
case c
of '<': result.add("&lt;")
of '>': result.add("&gt;")
of '&': result.add("&amp;")
of '"': result.add("&quot;")
of '\'': result.add("&#x27;")
of '/': result.add("&#x2F;")
else: result.add(c)
proc addIndent(result: var string, indent: int) =
result.add("\n")
for i in 1..indent: result.add(' ')
if n.kind == VNodeKind.text:
result.addEscaped(n.text)
elif n.kind == VNodeKind.verbatim:
result.add(n.text)
else:
let kind = $n.kind
result.add('<')
result.add(kind)
if n.id.len > 0:
result.add " id=\""
result.addEscapedAttr(n.id)
result.add('"')
if n.class.len > 0:
result.add " class=\""
result.addEscapedAttr(n.class)
result.add('"')
for k, v in attrs(n):
result.add(' ')
result.add(k)
result.add("=\"")
result.addEscapedAttr(v)
result.add('"')
if n.style != nil:
result.add " style=\""
for k, v in pairs(n.style):
if v.len == 0: continue
for t in tokenize($k, seps={'A' .. 'Z'}):
if t.isSep: result.add '-'
result.add toLowerAscii(t.token)
result.add ": "
result.add v
result.add "; "
result.add('"')
if n.len > 0:
result.add('>')
if n.len > 1:
var noWhitespace = false
for i in 0..<n.len:
if n[i].kind == VNodeKind.text:
noWhitespace = true
break
if noWhitespace:
# for mixed leaves, we cannot output whitespace for readability,
# because this would be wrong. For example: ``a<b>b</b>`` is
# different from ``a <b>b</b>``.
for i in 0..<n.len: result.add(n[i], indent+indWidth, indWidth)
else:
for i in 0..<n.len:
result.addIndent(indent+indWidth)
result.add(n[i], indent+indWidth, indWidth)
result.addIndent(indent)
else:
result.add(n[0], indent+indWidth, indWidth)
result.add("</")
result.add(kind)
result.add(">")
elif n.kind in selfClosing:
result.add(" />")
else:
result.add(">")
result.add("</")
result.add(kind)
result.add(">")
proc `$`*(n: VNode): kstring =
when defined(js):
var res = ""
toString(n, res, 0)
result = kstring(res)
else:
result = ""
add(result, n)
proc getVNodeById*(n: VNode; id: cstring): VNode =
## Get the VNode that was marked with ``id``. Returns ``nil``
## if no node exists.
if n.id == id: return n
for i in 0..<n.len:
result = getVNodeById(n[i], id)
if result != nil: return result

354
karax/vstyles.nim Normal file
View file

@ -0,0 +1,354 @@
##[
see examples/hellostyle.nim
]##
import std/[macros, strutils]
import kbase
when defined(js):
import kdom, jdict
type
StyleAttr* {.pure.} = enum
## The style attributes supported by the virtual DOM.
## Reference: https://www.w3schools.com/jsref/dom_obj_style.asp
alignContent
alignItems
alignSelf
animation
animationDelay
animationDirection
animationDuration
animationFillMode
animationIterationCount
animationName
animationTimingFunction
animationPlayState
background
backgroundAttachment
backgroundColor
backgroundImage
backgroundPosition
backgroundRepeat
backgroundClip
backgroundOrigin
backgroundSize
backfaceVisibility
border
borderBottom
borderBottomColor
borderBottomLeftRadius
borderBottomRightRadius
borderBottomStyle
borderBottomWidth
borderCollapse
borderColor
borderImage
borderImageOutset
borderImageRepeat
borderImageSlice
borderImageSource
borderImageWidth
borderLeft
borderLeftColor
borderLeftStyle
borderLeftWidth
borderRadius
borderRight
borderRightColor
borderRightStyle
borderRightWidth
borderSpacing
borderStyle
borderTop
borderTopColor
borderTopLeftRadius
borderTopRightRadius
borderTopStyle
borderTopWidth
borderWidth
bottom
boxDecorationBreak
boxShadow
boxSizing
captionSide
clear
clip
color
columnCount
columnFill
columnGap
columnRule
columnRuleColor
columnRuleStyle
columnRuleWidth
columns
columnSpan
columnWidth
content
counterIncrement
counterReset
cursor
direction
display
emptyCells
filter
flex
flexBasis
flexDirection
flexFlow
flexGrow
flexShrink
flexWrap
cssFloat
font
fontFamily
fontSize
fontSizeAdjust
fontStretch
fontStyle
fontVariant
fontWeight
hangingPunctuation
height
hyphens
icon
imageOrientation
justifyContent
left
letterSpacing
lineHeight
listStyle
listStyleImage
listStylePosition
listStyleType
margin
marginBottom
marginLeft
marginRight
marginTop
maxHeight
maxWidth
minHeight
minWidth
navDown
navIndex
navLeft
navRight
navUp
opacity
order
orphans
outline
outlineColor
outlineOffset
outlineStyle
outlineWidth
overflow
overflowX
overflowY
padding
paddingBottom
paddingLeft
paddingRight
paddingTop
pageBreakAfter
pageBreakBefore
pageBreakInside
perspective
perspectiveOrigin
pointerEvents
position
quotes
resize
right
scrollbar3dLightColor # note: missing in w3schools reference
scrollbarArrowColor # note: missing in w3schools reference
scrollbarBaseColor # note: missing in w3schools reference
scrollbarDarkshadowColor # note: missing in w3schools reference
scrollbarFaceColor # note: missing in w3schools reference
scrollbarHighlightColor # note: missing in w3schools reference
scrollbarShadowColor # note: missing in w3schools reference
scrollbarTrackColor # note: missing in w3schools reference
tableLayout
tabSize
textAlign
textAlignLast
textDecoration
textDecorationColor
textDecorationLine
textDecorationStyle
textIndent
textJustify
textOverflow
textShadow
textTransform
top
transform
transformOrigin
transformStyle
transition
transitionDelay
transitionDuration
transitionProperty
transitionTimingFunction
unicodeBidi
userSelect
verticalAlign
visibility
whiteSpace
width
wordBreak
wordSpacing
wordWrap
widows
zIndex
macro buildLookupTables(): untyped =
var e = newTree(nnkBracket)
for i in low(StyleAttr)..high(StyleAttr):
e.add(newCall("kstring", newLit($i)))
template tmpl(e) {.dirty.} =
const
toStyleAttrName: array[StyleAttr, kstring] = e
result = getAst tmpl(e)
buildLookupTables()
# I optimized the heck out of this representation since profiling showed
# it's relevant.
# even index: key, odd index: value; done this way for memory efficiency:
when defined(js):
type
VStyle* = JSeq[cstring]
else:
type
VStyle* = ref seq[string]
proc len*(a: VStyle): int = len(a[])
proc add*(a: VStyle; x: string) = add(a[], x)
proc eq*(a, b: VStyle): bool =
if a.isNil:
if b.isNil: return true
else: return false
elif b.isNil: return false
if a.len != b.len: return false
for i in 0..<a.len:
if a[i] != b[i]: return false
return true
proc setAttr*(s: VStyle; a, value: kstring) {.noSideEffect.} =
## inserts (a, value) in sorted order of key `a`
# worst case quadratic complexity (if given styles in reverse order), hopefully
# not a concern assuming small cardinal
var i = 0
while i < s.len:
if s[i] == a:
s[i+1] = value
return
elif s[i] > a:
s.add ""
s.add ""
# insertion point here, shift all remaining pairs by 2 indexes
for j in countdown(s.len-1, i+3, 2):
s[j] = s[j-2]
s[j-1] = s[j-3]
s[i] = a
s[i+1] = value
return
inc i, 2
s.add a
s.add value
proc setAttr*(s: VStyle; attr: StyleAttr, value: kstring) {.noSideEffect.} =
when kstring is cstring:
assert value != nil, "value must not be nil"
setAttr(s, toStyleAttrName[attr], value)
proc getAttr*(s: VStyle; attr: StyleAttr): kstring {.noSideEffect.} =
## returns "" if the attribute has not been set.
var i = 0
let a = toStyleAttrName[attr]
while i < s.len:
if s[i] == a:
return s[i+1]
elif s[i] > a:
return ""
inc i, 2
proc style*(pairs: varargs[(StyleAttr, kstring)]): VStyle {.noSideEffect.} =
## constructs a VStyle object from a list of (attribute, value)-pairs.
when defined(js):
result = newJSeq[cstring]()
else:
new(result)
result[] = @[]
for x in pairs:
result.setAttr x[0], x[1]
proc style*(a: StyleAttr; val: kstring): VStyle {.noSideEffect.} =
## constructs a VStyle object from a single (attribute, value)-pair.
when defined(js):
result = newJSeq[cstring]()
else:
new(result)
result[] = @[]
result.setAttr a, val
proc toCss*(a: string): VStyle =
##[
See example in hellostyle.nim
Allows passing a css string directly, eg:
tdiv(style = style((fontStyle, "italic".kstring), (color, "orange".kstring))): discard
tdiv(style = "font-style: oblique; color: pink".toCss): discard
]##
when defined(js):
result = newJSeq[cstring]()
else:
new(result)
result[] = @[]
for ai in a.split(";"):
var ai = ai.strip
if ai.len == 0: continue
let aj = ai.strip.split(":", maxsplit=1)
result.setAttr(aj[0], aj[1])
when defined(js):
proc setStyle(d: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.}
proc applyStyle*(n: Node; s: VStyle) {.noSideEffect.} =
## apply the style to the real DOM node ``n``.
#n.style = Style() # optimized, this is a hotspot:
{.emit: "`n`.style = {};".}
for i in countup(0, s.len-1, 2):
n.style.setStyle(s[i], s[i+1])
proc merge*(a, b: VStyle): VStyle {.noSideEffect.} =
## merges two styles. ``b`` takes precedence over ``a``.
when defined(js):
result = newJSeq[cstring]()
else:
new(result)
result[] = @[]
for i in 0..<a.len:
result.add a[i]
for i in countup(0, b.len-1, 2):
setAttr(result, b[i], b[i+1])
iterator pairs*(s: VStyle): (kstring, kstring) {.noSideEffect.} =
if s != nil:
for i in countup(0, s.len-1, 2):
yield (s[i], s[i+1])
when defined(js):
import jstrutils
else:
template `&`(x: untyped): untyped = $x
proc rgb*(r, g, b: range[0..255]): kstring =
kstring"rgb(" & &r & kstring", " & &g & kstring", " & &b & kstring")"
proc rgba*(r, g, b: range[0..255], alpha: float): kstring =
kstring"rgba(" & &r & kstring", " & &g & kstring", " & &b & kstring", " & &alpha & kstring")"

394
readme.md Normal file
View file

@ -0,0 +1,394 @@
![karax](https://user-images.githubusercontent.com/22755228/117183486-482b2a00-ade0-11eb-88e6-d8eeb28951ca.png)
![Github Actions](https://img.shields.io/github/workflow/status/karaxnim/karax/CI?style=for-the-badge) ![GitHub issues](https://img.shields.io/github/issues-raw/karaxnim/karax?style=for-the-badge) ![GitHub](https://img.shields.io/github/license/karaxnim/karax?style=for-the-badge) ![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/karaxnim/karax?sort=semver&style=for-the-badge) ![https://nim-lang.org](https://img.shields.io/badge/nim-powered-ffc200?style=for-the-badge)
# Karax
Karax is a framework for developing single page applications in Nim.
## Install
To use Karax you must have nim installed. You can follow the instructions [here](https://nim-lang.org/install.html).
Then you can install karax through nimble:
``nimble install karax``
## Try Karax
To try it out, run:
``cd ~/projects # Insert your favourite directory for projects``
``nimble develop karax # This will clone Karax and create a link to it in ~/.nimble``
``cd karax``
``cd examples/todoapp``
``nim js todoapp.nim``
``open todoapp.html``
``cd ../..``
``cd examples/mediaplayer``
``nim js playerapp.nim``
``open playerapp.html``
It uses a virtual DOM like React, but is much smaller than the existing
frameworks plus of course it's written in Nim for Nim. No external
dependencies! And thanks to Nim's whole program optimization only what
is used ends up in the generated JavaScript code.
## Goals
- Leverage Nim's macro system to produce a framework that allows
for the development of applications that are boilerplate free.
- Keep it small, keep it fast, keep it flexible.
## Hello World
The simplest Karax program looks like this:
```nim
include karax / prelude
proc createDom(): VNode =
result = buildHtml(tdiv):
text "Hello World!"
setRenderer createDom
```
Since ``div`` is a keyword in Nim, karax choose to use ``tdiv`` instead
here. ``tdiv`` produces a ``<div>`` virtual DOM node.
As you can see, karax comes with its own ``buildHtml`` DSL for convenient
construction of (virtual) DOM trees (of type ``VNode``). Karax provides
a tiny build tool called ``karun`` that generates the HTML boilerplate code that
embeds and invokes the generated JavaScript code::
``nim c karax/tools/karun``
``karax/tools/karun -r helloworld.nim``
Via ``-d:debugKaraxDsl`` we can have a look at the produced Nim code by
``buildHtml``:
```nim
let tmp1 = tree(VNodeKind.tdiv)
add(tmp1, text "Hello World!")
tmp1
```
(I shortened the IDs for better readability.)
Ok, so ``buildHtml`` introduces temporaries and calls ``add`` for the tree
construction so that it composes with all of Nim's control flow constructs:
```nim
include karax / prelude
import random
proc createDom(): VNode =
result = buildHtml(tdiv):
if rand(100) <= 50:
text "Hello World!"
else:
text "Hello Universe"
randomize()
setRenderer createDom
```
Produces:
```nim
let tmp1 = tree(VNodeKind.tdiv)
if rand(100) <= 50:
add(tmp1, text "Hello World!")
else:
add(tmp1, text "Hello Universe")
tmp1
```
## Event model
Karax does not change the DOM's event model much, here is a program
that writes "Hello simulated universe" on a button click:
```nim
include karax / prelude
# alternatively: import karax / [kbase, vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, jjson]
var lines: seq[kstring] = @[]
proc createDom(): VNode =
result = buildHtml(tdiv):
button:
text "Say hello!"
proc onclick(ev: Event; n: VNode) =
lines.add "Hello simulated universe"
for x in lines:
tdiv:
text x
setRenderer createDom
```
``kstring`` is Karax's alias for ``cstring`` (which stands for "compatible
string"; for the JS target that is an immutable JavaScript string) which
is preferred for efficiency on the JS target. However, on the native targets
``kstring`` is mapped to ``string`` for efficiency. The DSL for HTML
construction is also avaible for the native targets (!) and the ``kstring``
abstraction helps to deal with these conflicting requirements.
Karax's DSL is quite flexible when it comes to event handlers, so the
following syntax is also supported:
```nim
include karax / prelude
from sugar import `=>`
var lines: seq[kstring] = @[]
proc createDom(): VNode =
result = buildHtml(tdiv):
button(onclick = () => lines.add "Hello simulated universe"):
text "Say hello!"
for x in lines:
tdiv:
text x
setRenderer createDom
```
The ``buildHtml`` macro produces this code for us:
```nim
let tmp2 = tree(VNodeKind.tdiv)
let tmp3 = tree(VNodeKind.button)
addEventHandler(tmp3, EventKind.onclick,
() => lines.add "Hello simulated universe", kxi)
add(tmp3, text "Say hello!")
add(tmp2, tmp3)
for x in lines:
let tmp4 = tree(VNodeKind.tdiv)
add(tmp4, text x)
add(tmp2, tmp4)
tmp2
```
As the examples grow larger it becomes more and more visible of what
a DSL that composes with the builtin Nim control flow constructs buys us.
Once you have tasted this power there is no going back and languages
without AST based macro system simply don't cut it anymore.
## Attaching data to an event handler
Since the type of an event handler is ``(ev: Event; n: VNode)`` or ``()`` any
additional data that should be passed to the event handler needs to be
done via Nim's closures. In general this means a pattern like this:
```nim
proc menuAction(menuEntry: kstring): proc() =
result = proc() =
echo "clicked ", menuEntry
proc buildMenu(menu: seq[kstring]): VNode =
result = buildHtml(tdiv):
for m in menu:
nav(class="navbar is-primary"):
tdiv(class="navbar-brand"):
a(class="navbar-item", onclick = menuAction(m)):
```
## DOM diffing
Ok, so now we have seen DOM creation and event handlers. But how does
Karax actually keep the DOM up to date? The trick is that every event
handler is wrapped in a helper proc that triggers a *redraw* operation
that calls the *renderer* that you initially passed to ``setRenderer``.
So a new virtual DOM is created and compared against the previous
virtual DOM. This comparison produces a patch set that is then applied
to the real DOM the browser uses internally. This process is called
"virtual DOM diffing" and other frameworks, most notably Facebook's
*React*, do quite similar things. The virtual DOM is faster to create
and manipulate than the real DOM so this approach is quite efficient.
## Form validation
Most applications these days have some "login"
mechanism consisting of ``username`` and ``password`` and
a ``login`` button. The login button should only be clickable
if ``username`` and ``password`` are not empty. An error
message should be shown as long as one input field is empty.
To create new UI elements we write a ``loginField`` proc that
returns a ``VNode``:
```nim
proc loginField(desc, field, class: kstring;
validator: proc (field: kstring): proc ()): VNode =
result = buildHtml(tdiv):
label(`for` = field):
text desc
input(class = class, id = field, onchange = validator(field))
```
We use the ``karax / errors`` module to help with this error
logic. The ``errors`` module is mostly a mapping from strings to
strings but it turned out that the logic is tricky enough to warrant
a library solution. ``validateNotEmpty`` returns a closure that
captures the ``field`` parameter:
```nim
proc validateNotEmpty(field: kstring): proc () =
result = proc () =
let x = getVNodeById(field).getInputText
if x.isNil or x == "":
errors.setError(field, field & " must not be empty")
else:
errors.setError(field, "")
```
This indirection is required because
event handlers in Karax need to have the type ``proc ()``
or ``proc (ev: Event; n: VNode)``. The errors module also
gives us a handy ``disableOnError`` helper. It returns
``"disabled"`` if there are errors. Now we have all the
pieces together to write our login dialog:
```nim
# some consts in order to prevent typos:
const
username = kstring"username"
password = kstring"password"
var loggedIn: bool
proc loginDialog(): VNode =
result = buildHtml(tdiv):
if not loggedIn:
loginField("Name :", username, "input", validateNotEmpty)
loginField("Password: ", password, "password", validateNotEmpty)
button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()):
text "Login"
p:
text errors.getError(username)
p:
text errors.getError(password)
else:
p:
text "You are now logged in."
setRenderer loginDialog
```
(Full example [here](https://github.com/karaxnim/karax/blob/master/examples/login.nim).)
This code still has a bug though, when you run it, the ``login`` button is not
disabled until some input fields are validated! This is easily fixed,
at initialization we have to do:
```nim
setError username, username & " must not be empty"
setError password, password & " must not be empty"
```
There are likely more elegant solutions to this problem.
## Routing
For routing ``setRenderer`` can be called with a callback that takes a parameter of
type ``RouterData``. Here is the relevant excerpt from the famous "Todo App" example:
```nim
proc createDom(data: RouterData): VNode =
if data.hashPart == "#/": filter = all
elif data.hashPart == "#/completed": filter = completed
elif data.hashPart == "#/active": filter = active
result = buildHtml(tdiv(class="todomvc-wrapper")):
section(class = "todoapp"):
...
setRenderer createDom
```
(Full example [here](https://github.com/karaxnim/karax/blob/master/examples/todoapp/todoapp.nim).)
## Server Side HTML Rendering
Karax can also be used to render HTML on the server. Only a subset of
modules can be used since there is no JS interpreter.
```nim
import karax / [karaxdsl, vdom]
const places = @["boston", "cleveland", "los angeles", "new orleans"]
proc render*(): string =
let vnode = buildHtml(tdiv(class = "mt-3")):
h1: text "My Web Page"
p: text "Hello world"
ul:
for place in places:
li: text place
dl:
dt: text "Can I use Karax for client side single page apps?"
dd: text "Yes"
dt: text "Can I use Karax for server side HTML rendering?"
dd: text "Yes"
result = $vnode
echo render()
```
## Generate HTML with event handlers
If you are writing a static site generator or do server-side HTML rendering
via ``nim c``, you may want to override ``addEventHandler`` when using event
handlers to avoid compiler complaints.
Here's an example of auto submit a dropdown when a value is selected:
```nim
template kxi(): int = 0
template addEventHandler(n: VNode; k: EventKind; action: string; kxi: int) =
n.setAttr($k, action)
let
names = @["nim", "c", "python"]
selected_name = request.params.getOrDefault("name")
hello = buildHtml(html):
form(`method` = "get"):
select(name="name", onchange="this.form.submit()"):
for name in names:
if name == selected_name:
option(selected = ""): text name
else:
option: text name
```
## License
MIT License. See [here](https://github.com/karaxnim/karax/blob/master/LICENSE.txt).

View file

@ -1,26 +0,0 @@
Karax -- Single page applications in Nim
========================================
Karax is a framework for developing single page applications in Nim.
It's still in heavy development, so keep in mind that the API is subject
to change.
To try it out, run::
cd examples/todoapp
nim js todoapp.nim
open todoapp.html
It uses a virtual DOM like React, but is much smaller than the existing
frameworks plus of course it's written in Nim for Nim. No external
dependencies! And thanks to Nim's whole program optimization only what
is used ends up producing JavaScript code.
Goals
=====
- Leverage Nim's macro system to produce a framework that allows
for the development of applications that are boilerplate free.
- Keep it small, keep it fast, keep it flexible.

View file

@ -1,564 +0,0 @@
#
#
# Nim's Runtime Library
# (c) Copyright 2012 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## Declaration of the Document Object Model for the `JavaScript backend
## <backends.html#the-javascript-target>`_.
when not defined(js) and not defined(Nimdoc):
{.error: "This module only works on the JavaScript platform".}
type
EventTarget* = ref EventTargetObj
EventTargetObj {.importc.} = object of RootObj
onabort*: proc (event: Event) {.nimcall.}
onblur*: proc (event: Event) {.nimcall.}
onchange*: proc (event: Event) {.nimcall.}
onclick*: proc (event: Event) {.nimcall.}
ondblclick*: proc (event: Event) {.nimcall.}
onerror*: proc (event: Event) {.nimcall.}
onfocus*: proc (event: Event) {.nimcall.}
onkeydown*: proc (event: Event) {.nimcall.}
onkeypress*: proc (event: Event) {.nimcall.}
onkeyup*: proc (event: Event) {.nimcall.}
onload*: proc (event: Event) {.nimcall.}
onmousedown*: proc (event: Event) {.nimcall.}
onmousemove*: proc (event: Event) {.nimcall.}
onmouseout*: proc (event: Event) {.nimcall.}
onmouseover*: proc (event: Event) {.nimcall.}
onmouseup*: proc (event: Event) {.nimcall.}
onreset*: proc (event: Event) {.nimcall.}
onselect*: proc (event: Event) {.nimcall.}
onsubmit*: proc (event: Event) {.nimcall.}
onunload*: proc (event: Event) {.nimcall.}
Window* = ref WindowObj
WindowObj {.importc.} = object of EventTargetObj
document*: Document
event*: Event
history*: History
location*: Location
closed*: bool
defaultStatus*: cstring
innerHeight*, innerWidth*: int
locationbar*: ref TLocationBar
menubar*: ref TMenuBar
name*: cstring
outerHeight*, outerWidth*: int
pageXOffset*, pageYOffset*: int
personalbar*: ref TPersonalBar
scrollbars*: ref TScrollBars
statusbar*: ref TStatusBar
status*: cstring
toolbar*: ref TToolBar
frames*: seq[TFrame]
Frame* = ref FrameObj
FrameObj {.importc.} = object of WindowObj
ClassList* = ref ClassListObj
ClassListObj {.importc.} = object of RootObj
NodeType* = enum
ElementNode = 1,
AttributeNode,
TextNode,
CDATANode,
EntityRefNode,
EntityNode,
ProcessingInstructionNode,
CommentNode,
DocumentNode,
DocumentTypeNode,
DocumentFragmentNode,
NotationNode
Node* = ref NodeObj
NodeObj {.importc.} = object of EventTargetObj
attributes*: seq[Node]
childNodes*: seq[Node]
children*: seq[Node]
data*: cstring
firstChild*: Node
lastChild*: Node
nextSibling*: Node
nodeName*: cstring
nodeType*: NodeType
nodeValue*: cstring
parentNode*: Node
previousSibling*: Node
innerHTML*: cstring
style*: Style
Document* = ref DocumentObj
DocumentObj {.importc.} = object of NodeObj
alinkColor*: cstring
bgColor*: cstring
body*: Element
charset*: cstring
cookie*: cstring
defaultCharset*: cstring
fgColor*: cstring
head*: Element
lastModified*: cstring
linkColor*: cstring
referrer*: cstring
title*: cstring
URL*: cstring
vlinkColor*: cstring
anchors*: seq[AnchorElement]
forms*: seq[FormElement]
images*: seq[ImageElement]
applets*: seq[Element]
embeds*: seq[EmbedElement]
links*: seq[LinkElement]
Element* = ref ElementObj
ElementObj {.importc.} = object of NodeObj
classList*: Classlist
checked*: bool
defaultChecked*: bool
defaultValue*: cstring
disabled*: bool
form*: FormElement
name*: cstring
readOnly*: bool
options*: seq[OptionElement]
# https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement
HtmlElement* = ref object of Element
contentEditable*: string
isContentEditable*: bool
dir*: string
offsetHeight*: int
offsetWidth*: int
offsetLeft*: int
offsetTop*: int
LinkElement* = ref LinkObj
LinkObj {.importc.} = object of ElementObj
target*: cstring
text*: cstring
x*: int
y*: int
EmbedElement* = ref EmbedObj
EmbedObj {.importc.} = object of ElementObj
height*: int
hspace*: int
src*: cstring
width*: int
`type`*: cstring
vspace*: int
AnchorElement* = ref AnchorObj
AnchorObj {.importc.} = object of ElementObj
text*: cstring
x*, y*: int
OptionElement* = ref OptionObj
OptionObj {.importc.} = object of ElementObj
defaultSelected*: bool
selected*: bool
selectedIndex*: int
text*: cstring
value*: cstring
FormElement* = ref FormObj
FormObj {.importc.} = object of ElementObj
action*: cstring
encoding*: cstring
`method`*: cstring
target*: cstring
elements*: seq[Element]
ImageElement* = ref ImageObj
ImageObj {.importc.} = object of ElementObj
border*: int
complete*: bool
height*: int
hspace*: int
lowsrc*: cstring
src*: cstring
vspace*: int
width*: int
Style = ref StyleObj
StyleObj {.importc.} = object of RootObj
background*: cstring
backgroundAttachment*: cstring
backgroundColor*: cstring
backgroundImage*: cstring
backgroundPosition*: cstring
backgroundRepeat*: cstring
border*: cstring
borderBottom*: cstring
borderBottomColor*: cstring
borderBottomStyle*: cstring
borderBottomWidth*: cstring
borderColor*: cstring
borderLeft*: cstring
borderLeftColor*: cstring
borderLeftStyle*: cstring
borderLeftWidth*: cstring
borderRight*: cstring
borderRightColor*: cstring
borderRightStyle*: cstring
borderRightWidth*: cstring
borderStyle*: cstring
borderTop*: cstring
borderTopColor*: cstring
borderTopStyle*: cstring
borderTopWidth*: cstring
borderWidth*: cstring
bottom*: cstring
captionSide*: cstring
clear*: cstring
clip*: cstring
color*: cstring
cursor*: cstring
direction*: cstring
display*: cstring
emptyCells*: cstring
cssFloat*: cstring
font*: cstring
fontFamily*: cstring
fontSize*: cstring
fontStretch*: cstring
fontStyle*: cstring
fontVariant*: cstring
fontWeight*: cstring
height*: cstring
left*: cstring
letterSpacing*: cstring
lineHeight*: cstring
listStyle*: cstring
listStyleImage*: cstring
listStylePosition*: cstring
listStyleType*: cstring
margin*: cstring
marginBottom*: cstring
marginLeft*: cstring
marginRight*: cstring
marginTop*: cstring
maxHeight*: cstring
maxWidth*: cstring
minHeight*: cstring
minWidth*: cstring
overflow*: cstring
padding*: cstring
paddingBottom*: cstring
paddingLeft*: cstring
paddingRight*: cstring
paddingTop*: cstring
pageBreakAfter*: cstring
pageBreakBefore*: cstring
position*: cstring
right*: cstring
scrollbar3dLightColor*: cstring
scrollbarArrowColor*: cstring
scrollbarBaseColor*: cstring
scrollbarDarkshadowColor*: cstring
scrollbarFaceColor*: cstring
scrollbarHighlightColor*: cstring
scrollbarShadowColor*: cstring
scrollbarTrackColor*: cstring
tableLayout*: cstring
textAlign*: cstring
textDecoration*: cstring
textIndent*: cstring
textTransform*: cstring
top*: cstring
verticalAlign*: cstring
visibility*: cstring
width*: cstring
wordSpacing*: cstring
zIndex*: int
# TODO: A lot of the fields in Event belong to a more specific type of event.
# TODO: Should we clean this up?
Event* = ref EventObj
EventObj {.importc.} = object of RootObj
target*: Node
altKey*, ctrlKey*, shiftKey*: bool
button*: int
clientX*, clientY*: int
keyCode*: int
layerX*, layerY*: int
modifiers*: int
ALT_MASK*, CONTROL_MASK*, SHIFT_MASK*, META_MASK*: int
offsetX*, offsetY*: int
pageX*, pageY*: int
screenX*, screenY*: int
which*: int
`type`*: cstring
x*, y*: int
ABORT*: int
BLUR*: int
CHANGE*: int
CLICK*: int
DBLCLICK*: int
DRAGDROP*: int
ERROR*: int
FOCUS*: int
KEYDOWN*: int
KEYPRESS*: int
KEYUP*: int
LOAD*: int
MOUSEDOWN*: int
MOUSEMOVE*: int
MOUSEOUT*: int
MOUSEOVER*: int
MOUSEUP*: int
MOVE*: int
RESET*: int
RESIZE*: int
SELECT*: int
SUBMIT*: int
UNLOAD*: int
TouchList* {.importc.} = ref object of RootObj
length*: int
TouchEvent* {.importc.} = ref object of Event
changedTouches*, targetTouches*, touches*: TouchList
Touch* {.importc.} = ref object of RootObj
identifier*: int
screenX*, screenY*, clientX*, clientY*, pageX*, pageY*: int
target*: Element
radiusX*, radiusY*: int
rotationAngle*: int
force*: float
Location* = ref LocationObj
LocationObj {.importc.} = object of RootObj
hash*: cstring
host*: cstring
hostname*: cstring
href*: cstring
pathname*: cstring
port*: cstring
protocol*: cstring
search*: cstring
History* = ref HistoryObj
HistoryObj {.importc.} = object of RootObj
length*: int
Navigator* = ref NavigatorObj
NavigatorObj {.importc.} = object of RootObj
appCodeName*: cstring
appName*: cstring
appVersion*: cstring
cookieEnabled*: bool
language*: cstring
platform*: cstring
userAgent*: cstring
mimeTypes*: seq[ref TMimeType]
TPlugin* {.importc.} = object of RootObj
description*: cstring
filename*: cstring
name*: cstring
TMimeType* {.importc.} = object of RootObj
description*: cstring
enabledPlugin*: ref TPlugin
suffixes*: seq[cstring]
`type`*: cstring
TLocationBar* {.importc.} = object of RootObj
visible*: bool
TMenuBar* = TLocationBar
TPersonalBar* = TLocationBar
TScrollBars* = TLocationBar
TToolBar* = TLocationBar
TStatusBar* = TLocationBar
Screen = ref ScreenObj
ScreenObj {.importc.} = object of RootObj
availHeight*: int
availWidth*: int
colorDepth*: int
height*: int
pixelDepth*: int
width*: int
TTimeOut* {.importc.} = object of RootObj
TInterval* {.importc.} = object of RootObj
{.push importcpp.}
# EventTarget "methods"
proc addEventListener*(et: EventTarget, ev: cstring, cb: proc(ev: Event), useCapture: bool = false)
# Window "methods"
proc alert*(w: Window, msg: cstring)
proc back*(w: Window)
proc blur*(w: Window)
proc captureEvents*(w: Window, eventMask: int) {.deprecated.}
proc clearInterval*(w: Window, interval: ref TInterval)
proc clearTimeout*(w: Window, timeout: ref TTimeOut)
proc close*(w: Window)
proc confirm*(w: Window, msg: cstring): bool
proc disableExternalCapture*(w: Window)
proc enableExternalCapture*(w: Window)
proc find*(w: Window, text: cstring, caseSensitive = false,
backwards = false)
proc focus*(w: Window)
proc forward*(w: Window)
proc handleEvent*(w: Window, e: Event)
proc home*(w: Window)
proc moveBy*(w: Window, x, y: int)
proc moveTo*(w: Window, x, y: int)
proc open*(w: Window, uri, windowname: cstring,
properties: cstring = nil): Window
proc print*(w: Window)
proc prompt*(w: Window, text, default: cstring): cstring
proc releaseEvents*(w: Window, eventMask: int) {.deprecated.}
proc resizeBy*(w: Window, x, y: int)
proc resizeTo*(w: Window, x, y: int)
proc routeEvent*(w: Window, event: Event)
proc scrollBy*(w: Window, x, y: int)
proc scrollTo*(w: Window, x, y: int)
proc setInterval*(w: Window, code: cstring, pause: int): ref TInterval
proc setInterval*(w: Window, function: proc (), pause: int): ref TInterval
proc setTimeout*(w: Window, code: cstring, pause: int): ref TTimeOut
proc setTimeout*(w: Window, function: proc (), pause: int): ref TInterval
proc stop*(w: Window)
proc requestAnimationFrame*(w: Window, function: proc (time: float)): int
proc cancelAnimationFrame*(w: Window, id: int)
# Node "methods"
proc appendChild*(n, child: Node)
proc appendData*(n: Node, data: cstring)
proc cloneNode*(n: Node, copyContent: bool): Node
proc deleteData*(n: Node, start, len: int)
proc getAttribute*(n: Node, attr: cstring): cstring
proc getAttributeNode*(n: Node, attr: cstring): Node
proc hasChildNodes*(n: Node): bool
proc insertBefore*(n, newNode, before: Node)
proc insertData*(n: Node, position: int, data: cstring)
proc removeAttribute*(n: Node, attr: cstring)
proc removeAttributeNode*(n, attr: Node)
proc removeChild*(n, child: Node)
proc replaceChild*(n, newNode, oldNode: Node)
proc replaceData*(n: Node, start, len: int, text: cstring)
proc scrollIntoView*(n: Node)
proc setAttribute*(n: Node, name, value: cstring)
proc setAttributeNode*(n: Node, attr: Node)
# Document "methods"
proc captureEvents*(d: Document, eventMask: int) {.deprecated.}
proc createAttribute*(d: Document, identifier: cstring): Node
proc createElement*(d: Document, identifier: cstring): Element
proc createTextNode*(d: Document, identifier: cstring): Node
proc getElementById*(d: Document, id: cstring): Element
proc getElementsByName*(d: Document, name: cstring): seq[Element]
proc getElementsByTagName*(d: Document, name: cstring): seq[Element]
proc getElementsByClassName*(d: Document, name: cstring): seq[Element]
proc getSelection*(d: Document): cstring
proc handleEvent*(d: Document, event: Event)
proc open*(d: Document)
proc releaseEvents*(d: Document, eventMask: int) {.deprecated.}
proc routeEvent*(d: Document, event: Event)
proc write*(d: Document, text: cstring)
proc writeln*(d: Document, text: cstring)
# Element "methods"
proc blur*(e: Element)
proc click*(e: Element)
proc focus*(e: Node)
proc handleEvent*(e: Element, event: Event)
proc select*(e: Element)
proc getElementsByTagName*(e: Element, name: cstring): seq[Element]
proc getElementsByClassName*(e: Element, name: cstring): seq[Element]
# FormElement "methods"
proc reset*(f: FormElement)
proc submit*(f: FormElement)
# EmbedElement "methods"
proc play*(e: EmbedElement)
proc stop*(e: EmbedElement)
# Location "methods"
proc reload*(loc: Location)
proc replace*(loc: Location, s: cstring)
# History "methods"
proc back*(h: History)
proc forward*(h: History)
proc go*(h: History, pagesToJump: int)
# Navigator "methods"
proc javaEnabled*(h: Navigator): bool
# ClassList "methods"
proc add*(c: ClassList, class: cstring)
proc remove*(c: ClassList, class: cstring)
proc contains*(c: ClassList, class: cstring):bool
proc toggle*(c: ClassList, class: cstring)
# Style "methods"
proc getAttribute*(s: Style, attr: cstring, caseSensitive=false): cstring
proc removeAttribute*(s: Style, attr: cstring, caseSensitive=false)
proc setAttribute*(s: Style, attr, value: cstring, caseSensitive=false)
# Event "methods"
proc preventDefault*(ev: Event)
# TouchEvent "methods"
proc identifiedTouch*(list: TouchList): Touch
proc item*(list: TouchList, i: int): Touch
{.pop.}
var
window* {.importc, nodecl.}: Window
document* {.importc, nodecl.}: Document
navigator* {.importc, nodecl.}: Navigator
screen* {.importc, nodecl.}: Screen
proc decodeURI*(uri: cstring): cstring {.importc, nodecl.}
proc encodeURI*(uri: cstring): cstring {.importc, nodecl.}
proc escape*(uri: cstring): cstring {.importc, nodecl.}
proc unescape*(uri: cstring): cstring {.importc, nodecl.}
proc decodeURIComponent*(uri: cstring): cstring {.importc, nodecl.}
proc encodeURIComponent*(uri: cstring): cstring {.importc, nodecl.}
proc isFinite*(x: BiggestFloat): bool {.importc, nodecl.}
proc isNaN*(x: BiggestFloat): bool {.importc, nodecl.}
proc parseFloat*(s: cstring): BiggestFloat {.importc, nodecl.}
proc parseInt*(s: cstring): int {.importc, nodecl.}
proc parseInt*(s: cstring, radix: int):int {.importc, nodecl.}
type
TEventHandlers* {.deprecated.} = EventTargetObj
TWindow* {.deprecated.} = WindowObj
TFrame* {.deprecated.} = FrameObj
TNode* {.deprecated.} = NodeObj
TDocument* {.deprecated.} = DocumentObj
TElement* {.deprecated.} = ElementObj
TLink* {.deprecated.} = LinkObj
TEmbed* {.deprecated.} = EmbedObj
TAnchor* {.deprecated.} = AnchorObj
TOption* {.deprecated.} = OptionObj
TForm* {.deprecated.} = FormObj
TImage* {.deprecated.} = ImageObj
TNodeType* {.deprecated.} = NodeType
TEvent* {.deprecated.} = EventObj
TLocation* {.deprecated.} = LocationObj
THistory* {.deprecated.} = HistoryObj
TNavigator* {.deprecated.} = NavigatorObj
TStyle* {.deprecated.} = StyleObj
TScreen* {.deprecated.} = ScreenObj
TApplet* {.importc, deprecated.} = object of RootObj

View file

@ -1,13 +0,0 @@
type
JDict*[K, V] = ref object
proc `[]`*[K, V](d: JDict[K, V], k: K): V {.importcpp: "#[#]".}
proc `[]=`*[K, V](d: JDict[K, V], k: K, v: V) {.importcpp: "#[#] = #".}
proc newJDict*[K, V](): JDict[K, V] {.importcpp: "{@}".}
proc contains*[K, V](d: JDict[K, V], k: K): bool {.importcpp: "#.hasOwnProperty(#)".}
proc del*[K, V](d: JDict[K, V], k: K) {.importcpp: "delete #[#]".}

View file

@ -1,523 +0,0 @@
# Simple lib to write JS UIs
import dom, vdom, jstrutils
export dom.Event, dom.cloneNode, dom
proc len(x: Node): int {.importcpp: "#.childNodes.length".}
proc `[]`(x: Node; idx: int): Element {.importcpp: "#.childNodes[#]".}
proc kout*[T](x: T) {.importc: "console.log", varargs.}
## the preferred way of debugging karax applications.
proc id*(e: Node): cstring {.importcpp: "#.id", nodecl.}
proc `id=`*(e: Node; x: cstring) {.importcpp: "#.id = #", nodecl.}
proc class*(e: Node): cstring {.importcpp: "#.className", nodecl.}
proc `class=`*(e: Node; v: cstring) {.importcpp: "#.className = #", nodecl.}
proc value*(e: Node): cstring {.importcpp: "#.value", nodecl.}
proc `value=`*(e: Node; v: cstring) {.importcpp: "#.value = #", nodecl.}
proc `disabled=`*(e: Node; v: bool) {.importcpp: "#.disabled = #", nodecl.}
proc getElementsByClass*(e: Node; name: cstring): seq[Node] {.
importcpp: "#.getElementsByClassName(#)", nodecl.}
proc hasProp(e: Node; prop: cstring): bool {.importcpp: "(#.hasOwnProperty(#))".}
proc rawkey(e: Node): VKey {.importcpp: "#.karaxKey", nodecl.}
proc key*(e: Node): VKey =
if e.hasProp"karaxKey": result = e.rawkey
else: result = -1
proc `key=`*(e: Node; x: VKey) {.importcpp: "#.karaxKey = #", nodecl.}
type
BoundingRect {.importc.} = object
top, bottom, left, right: int
proc getBoundingClientRect(e: Node): BoundingRect {.
importcpp: "getBoundingClientRect", nodecl.}
proc clientHeight(): int {.
importcpp: "(window.innerHeight || document.documentElement.clientHeight)@", nodecl}
proc clientWidth(): int {.
importcpp: "(window.innerWidth || document.documentElement.clientWidth)@", nodecl}
when false:
proc pageYOffset(): int {.
importcpp: "(window.pageYOffset)@", nodecl}
proc pageXOffset(): int {.
importcpp: "(window.pageXOffset)@", nodecl}
type
Timeout* = ref object
var
document* {.importc.}: Document
toFocus: Node
toFocusV: VNode
proc setFocus*(n: VNode) =
toFocusV = n
proc isElementInViewport(el: Node; h: var int): bool =
let rect = el.getBoundingClientRect()
h = rect.bottom - rect.top
result = rect.top >= 0 and rect.left >= 0 and
rect.bottom <= clientHeight() and
rect.right <= clientWidth()
proc vnodeToDom(n: VNode): Node =
if n.kind == VNodeKind.text:
result = document.createTextNode(n.text)
elif n.kind == VNodeKind.thunk:
return vnodeToDom(n.callThunk)
else:
result = document.createElement(toTag[n.kind])
for k in n:
appendChild(result, vnodeToDom(k))
# text is mapped to 'value':
if n.text != nil:
result.value = n.text
if n.id != nil:
result.id = n.id
if n.class != nil:
result.class = n.class
if n.key >= 0:
result.key = n.key
for k, v in attrs(n):
if v != nil:
result.setAttribute(k, v)
let myn = n
for e, h in items(n.events):
proc wrapper(): proc (ev: Event) =
let hh = h
result = proc (ev: Event) =
assert myn != nil
hh(ev, myn)
result.addEventListener(toEventName[e], wrapper())
if n == toFocusV and toFocus.isNil:
toFocus = result
proc same(n: VNode, e: Node): bool =
if toTag[n.kind] == e.nodename:
result = true
if n.kind != VNodeKind.text:
if e.len != n.len: return false
for i in 0 ..< n.len:
if not same(n[i], e[i]): return false
var
dorender: proc (): VNode {.closure.}
drawTimeout: Timeout
currentTree: VNode
proc setRenderer*(renderer: proc (): VNode) =
dorender = renderer
proc setTimeout*(action: proc(); ms: int): Timeout {.importc, nodecl.}
proc clearTimeout*(t: Timeout) {.importc, nodecl.}
#proc targetElem*(e: Event): Element = cast[Element](e.target)
proc getElementById*(id: cstring): Element {.importc: "document.getElementById", nodecl.}
#proc getElementsByClassName*(cls: cstring): seq[Element] {.importc:
# "document.getElementsByClassName", nodecl.}
proc textContent(e: Node): cstring {.
importcpp: "#.textContent", nodecl.}
proc replaceById(id: cstring; newTree: Node) =
let x = document.getElementById(id)
x.parentNode.replaceChild(newTree, x)
#newTree.id = id
proc equals(a, b: VNode): bool =
if a.kind != b.kind: return false
if a.id != b.id: return false
if a.key != b.key: return false
if a.kind == VNodeKind.text:
if a.text != b.text: return false
elif a.kind == VNodeKind.thunk:
if a.thunk != b.thunk: return false
if a.len != b.len: return false
for i in 0..<a.len:
if not equals(a[i], b[i]): return false
if not sameAttrs(a, b): return false
if a.class != b.class: return false
# XXX test event listeners here?
# --> maybe give nodes a hash?
return true
proc equalsTree(a, b: VNode): bool =
if not a.validHash:
a.calcHash()
if not b.validHash:
b.calcHash()
return a.hash == b.hash
proc updateElement(parent, current: Node, newNode, oldNode: VNode) =
if not equals(newNode, oldNode):
let n = vnodeToDom(newNode)
if parent == nil:
replaceById("ROOT", n)
else:
parent.replaceChild(n, current)
elif newNode.kind != VNodeKind.text:
let newLength = newNode.len
var oldLength = oldNode.len
let minLength = min(newLength, oldLength)
assert oldNode.kind == newNode.kind
when defined(simpleDiff):
for i in 0..min(newLength, oldLength)-1:
updateElement(current, current[i], newNode[i], oldNode[i])
if newLength > oldLength:
for i in oldLength..newLength-1:
current.appendChild(vnodeToDom(newNode[i]))
elif oldLength > newLength:
for i in countdown(oldLength-1, newLength):
current.removeChild(current.lastChild)
else:
var commonPrefix = 0
while commonPrefix < minLength and equalsTree(newNode[commonPrefix], oldNode[commonPrefix]):
inc commonPrefix
var oldPos = oldLength - 1
var newPos = newLength - 1
while oldPos >= commonPrefix and newPos >= commonPrefix and equalsTree(newNode[newPos], oldNode[oldPos]):
dec oldPos
dec newPos
var pos = min(oldPos, newPos) + 1
for i in commonPrefix..pos-1:
updateElement(current, current.childNodes[i],
newNode[i],
oldNode[i])
var nextChildPos = oldPos + 1
while pos <= newPos:
if nextChildPos == oldLength:
current.appendChild(vnodeToDom(newNode[pos]))
else:
current.insertBefore(vnodeToDom(newNode[pos]), current.childNodes[nextChildPos])
# added new Node, so old state of VDOM have one more Node
inc oldLength
inc pos
inc nextChildPos
for i in 0..oldPos-pos:
current.removeChild(current.childNodes[pos])
proc dodraw() =
let newtree = dorender()
newtree.id = "ROOT"
toFocus = nil
if currentTree == nil:
currentTree = newtree
let asdom = vnodeToDom currentTree
replaceById("ROOT", asdom)
else:
let olddom = document.getElementById("ROOT")
updateElement(nil, olddom, newtree, currentTree)
#assert same(newtree, document.getElementById("ROOT"))
currentTree = newtree
# now that it's part of the DOM, give it the focus:
if toFocus != nil:
toFocus.focus()
proc visibleKeys(e: Node; a, b: var VKey; h, count: var int) =
# we only care about nodes that have a key:
var hh = 0
# do not recurse if there is a 'key' field already:
if e.key >= 0:
if isElementInViewport(e, hh):
inc count
inc h, hh
a = min(a, e.key)
b = max(b, e.key)
else:
for i in 0..<e.len:
visibleKeys(e[i], a, b, h, count)
proc reqFrame(callback: proc()) {.importc: "window.requestAnimationFrame".}
proc redraw*() =
# we buffer redraw requests:
when false:
if drawTimeout != nil:
clearTimeout(drawTimeout)
drawTimeout = setTimeout(dodraw, 30)
elif true:
reqFrame(dodraw)
else:
dodraw()
proc init*() =
reqFrame(dodraw)
#proc prepend*(parent, kid: Element) =
# parent.insertBefore(kid, parent.firstChild)
# prependChild(parent, kid)
proc suffix*(s, prefix: cstring): cstring =
if s.startsWith(prefix):
result = s.substr(prefix.len)
else:
kout(cstring"bug! " & s & cstring" does not start with " & prefix)
proc suffixAsInt*(s, prefix: cstring): int = parseInt(suffix(s, prefix))
proc scrollTop*(e: Node): int {.importcpp: "#.scrollTop", nodecl.}
proc offsetHeight*(e: Node): int {.importcpp: "#.offsetHeight", nodecl.}
proc offsetTop*(e: Node): int {.importcpp: "#.offsetTop", nodecl.}
template onImpl(s) {.dirty.} =
proc wrapper(ev: Event; n: VNode) =
action(ev, n)
redraw()
addEventListener(e, s, wrapper)
proc setOnclick*(e: VNode; action: EventHandler) =
onImpl EventKind.onclick
proc setOnDblclick*(e: VNode; action: EventHandler) =
onImpl EventKind.ondblclick
proc setOnfocuslost*(e: VNode; action: EventHandler) =
onImpl EventKind.onblur
proc setOnchanged*(e: VNode; action: EventHandler) =
onImpl EventKind.onchange
proc setOnscroll*(e: VNode; action: EventHandler) =
onImpl EventKind.onscroll
#proc ceil(f: float): int {.importc: "Math.ceil", nodecl.}
proc setOnscroll*(action: proc(min, max: VKey; diff: int)) =
var oldY = window.pageYOffset
proc wrapper(ev: Event) =
let dir = window.pageYOffset - oldY
if dir == 0: return
var a = VKey high(int)
var b = VKey 0
var h, count: int
document.visibleKeys(a, b, h, count)
let avgh = h / count
let diff = toInt(dir.float / avgh)
if diff != 0:
oldY = window.pageYOffset
action(a, b, diff)
redraw()
document.addEventListener("scroll", wrapper)
proc setOnHashChange*(action: proc (hashPart: cstring)) =
var onhashChange {.importc: "window.onhashchange".}: proc()
var hashPart {.importc: "window.location.hash".}: cstring
proc wrapper() =
action(hashPart)
redraw()
onhashchange = wrapper
var
linkCounter: int
proc link*(id: int): VNode =
result = newVNode(VNodeKind.anchor)
result.setAttr("href", "#")
inc linkCounter
result.setAttr("id", $linkCounter & ":" & $id)
proc link*(action: EventHandler): VNode =
result = newVNode(VNodeKind.anchor)
result.setAttr("href", "#")
result.setOnclick action
when false:
proc button*(caption: cstring; action: EventHandler; disabled=false): VNode =
result = newVNode(VNodeKind.button)
result.add text(caption)
if action != nil:
result.setOnClick action
if disabled:
result.setAttr("disabled", "true")
proc select*(choices: openarray[cstring]): VNode =
result = newVNode(VNodeKind.select)
var i = 0
for c in choices:
result.add tree(VNodeKind.option, [(cstring"value", toCstr(i))], text(c))
inc i
proc select*(choices: openarray[(int, cstring)]): VNode =
result = newVNode(VNodeKind.select)
for c in choices:
result.add tree(VNodeKind.option, [(cstring"value", toCstr(c[0]))], text(c[1]))
var radioCounter: int
proc radio*(choices: openarray[(int, cstring)]): VNode =
result = newVNode(VNodeKind.fieldset)
var i = 0
inc radioCounter
for c in choices:
let id = cstring"radio_" & c[1] & toCstr(i)
var kid = tree(VNodeKind.input, [(cstring"type", cstring"radio"),
(cstring"id", id), (cstring"name", cstring"radio" & toCStr(radioCounter)),
(cstring"value", toCStr(c[0]))])
if i == 0:
kid.setAttr(cstring"checked", cstring"checked")
var lab = tree(VNodeKind.label, [(cstring"for", id)], text(c[1]))
kid.add lab
result.add kid
inc i
proc tag*(kind: VNodeKind; id=cstring(nil), class=cstring(nil)): VNode =
result = newVNode(kind)
result.id = id
result.class = class
proc tdiv*(id=cstring(nil), class=cstring(nil)): VNode = tag(VNodeKind.tdiv, id, class)
proc span*(id=cstring(nil), class=cstring(nil)): VNode = tag(VNodeKind.span, id, class)
proc valueAsInt*(e: Node): int = parseInt(e.value)
proc th*(s: cstring): VNode =
result = newVNode(VNodeKind.th)
result.add text(s)
proc td*(s: string): VNode =
result = newVNode(VNodeKind.td)
result.add text(s)
proc td*(s: VNode): VNode =
result = newVNode(VNodeKind.td)
result.add s
proc td*(class: cstring; s: VNode): VNode =
result = newVNode(VNodeKind.td)
result.add s
result.class = class
proc table*(class=cstring(nil), kids: varargs[VNode]): VNode =
result = tag(VNodeKind.table, nil, class)
for k in kids: result.add k
proc tr*(kids: varargs[VNode]): VNode =
result = newVNode(VNodeKind.tr)
for k in kids:
if k.kind in {VNodeKind.td, VNodeKind.th}:
result.add k
else:
result.add td(k)
proc getAttr(e: Node; key: cstring): cstring {.
importcpp: "#.getAttribute(#)", nodecl.}
template nativeValue(ev): cstring = cast[Element](ev.target).value
template setNativeValue(ev, val) = cast[Element](ev.target).value = val
template keyeventBody() =
n.value = nativeValue(ev)
action(ev, n)
setNativeValue(ev, n.value)
redraw()
proc realtimeInput*(val: cstring; action: EventHandler): VNode =
#let oldElem = getElementById(id)
#if oldElem != nil: return oldElem
#let newVal = if oldElem.isNil: val else: $oldElem.value
var timer: Timeout
proc onkeyup(ev: Event; n: VNode) =
proc wrapper() = keyeventBody()
if timer != nil: clearTimeout(timer)
timer = setTimeout(wrapper, 400)
result = tree(VNodeKind.input, [(cstring"type", cstring"text")])
result.value = val
result.addEventListener(EventKind.onkeyup, onkeyup)
proc enterInput*(id, val: cstring; action: EventHandler): VNode =
#let oldElem = getElementById(id)
#if oldElem != nil: return oldElem
#let newVal = if oldElem.isNil: val else: $oldElem.value
proc onkeyup(ev: Event; n: VNode) =
if ev.keyCode == 13: keyeventBody()
result = tree(VNodeKind.input, [(cstring"type", cstring"text")])
result.id = id
result.value = val
result.addEventListener(EventKind.onkeyup, onkeyup)
proc setOnEnter*(n: VNode; action: EventHandler) =
proc onkeyup(ev: Event; n: VNode) =
if ev.keyCode == 13: keyeventBody()
n.addEventListener(EventKind.onkeyup, onkeyup)
proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)];
data: cstring;
cont: proc (httpStatus: int; response: cstring)) =
proc setRequestHeader(a, b: cstring) {.importc: "ajax.setRequestHeader".}
{.emit: """
var ajax = new XMLHttpRequest();
ajax.open(`meth`,`url`,true);""".}
for a, b in items(headers):
setRequestHeader(a, b)
{.emit: """
ajax.onreadystatechange = function(){
if(this.readyState == 4){
if(this.status == 200){
`cont`(this.status, this.responseText);
} else {
`cont`(this.status, this.statusText);
}
}
}
ajax.send(`data`);
""".}
proc ajaxPut*(url: string; headers: openarray[(cstring, cstring)];
data: cstring;
cont: proc (httpStatus: int, response: cstring)) =
ajax("PUT", url, headers, data, cont)
proc ajaxGet*(url: string; headers: openarray[(cstring, cstring)];
cont: proc (httpStatus: int, response: cstring)) =
ajax("GET", url, headers, nil, cont)
{.push stackTrace:off.}
proc setupErrorHandler*(useAlert=false) =
## Installs an error handler that transforms native JS unhandled
## exceptions into Nim based stack traces. If `useAlert` is false,
## the error message is put into the console, otherwise `alert`
## is called.
proc stackTraceAsCstring(): cstring = cstring(getStackTrace())
{.emit: """
window.onerror = function(msg, url, line, col, error) {
var x = "Error: " + msg + "\n" + `stackTraceAsCstring`()
if (`useAlert`)
alert(x);
else
console.log(x);
var suppressErrorAlert = true;
return suppressErrorAlert;
};""".}
{.pop.}
when false:
var plugins {.exportc.}: seq[(string, proc())] = @[]
proc onInput(val: cstring) =
kout val
if val == "dyn":
let body = getElementById("body")
body.prepend(tree("script", [("type", "text/javascript"), ("src", "nimcache/dyn.js")]))
redraw()
kout(plugins.len)
if plugins.len > 0:
plugins[0][1]()

View file

@ -1,113 +0,0 @@
import macros, karax, vdom
from strutils import startsWith, toLowerAscii
const
StmtContext = ["kout", "inc", "echo", "dec", "!"]
SpecialAttrs = ["id", "class", "value", "key"]
proc getName(n: NimNode): string =
case n.kind
of nnkIdent:
result = $n.ident
of nnkAccQuoted:
result = ""
for i in 0..<n.len:
result.add getName(n[i])
of nnkStrLit..nnkTripleStrLit:
result = n.strVal
else:
#echo repr n
expectKind(n, nnkIdent)
proc newDotAsgn(tmp: NimNode, key: string, x: NimNode): NimNode =
result = newTree(nnkAsgn, newDotExpr(tmp, newIdentNode key), x)
proc isTag(s: string): bool =
for i in VNodeKind.low.succ..VNodeKind.high:
if $i == s: return true
proc tcall2(n, tmpContext: NimNode): NimNode =
# we need to distinguish statement and expression contexts:
# every call statement 's' needs to be transformed to 'dest.add s'.
# If expressions need to be distinguished from if statements. Since
# we know we start in a statement context, it's pretty simple to
# figure out expression contexts: In calls everything is an expression
# (except for the last child of the macros we consider here),
# lets, consts, types can be considered as expressions
# case is complex, calls are assumed to produce a value.
case n.kind
of nnkLiterals, nnkIdent, nnkSym, nnkDotExpr, nnkBracketExpr:
if tmpContext != nil:
result = newCall(bindSym"add", tmpContext, n)
else:
result = n
of nnkForStmt, nnkIfExpr, nnkElifExpr, nnkElseExpr,
nnkOfBranch, nnkElifBranch, nnkExceptBranch, nnkElse,
nnkConstDef, nnkWhileStmt, nnkIdentDefs, nnkVarTuple:
# recurse for the last son:
result = copyNimTree(n)
let L = n.len
assert n.len == result.len
if L > 0:
result[L-1] = tcall2(result[L-1], tmpContext)
of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkCaseStmt,
nnkTryStmt, nnkFinally:
# recurse for every child:
result = copyNimNode(n)
for x in n:
result.add tcall2(x, tmpContext)
of nnkVarSection, nnkLetSection, nnkConstSection:
result = n
of nnkCallKinds:
let op = getName(n[0])
if isTag(op):
let tmp = genSym(nskLet, "tmp")
result = newTree(
if tmpContext == nil: nnkStmtListExpr else: nnkStmtList,
newLetStmt(tmp, newCall(bindSym"tree", newDotExpr(bindSym"VNodeKind", n[0]))))
for i in 1 ..< n.len:
# named parameters are transformed into attributes or events:
let x = n[i]
if x.kind == nnkExprEqExpr:
let key = getName x[0]
if key.startsWith("on"):
result.add newCall(!("set" & key), tmp, x[1])
elif key in SpecialAttrs:
result.add newDotAsgn(tmp, key, x[1])
else:
result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
elif x.kind == nnkIdent:
result.add newCall(x, tmp)
else:
result.add tcall2(x, tmp)
if tmpContext == nil:
result.add tmp
else:
result.add newCall(bindSym"add", tmpContext, tmp)
elif tmpContext != nil and op notin StmtContext:
result = newCall(bindSym"add", tmpContext, n)
elif op == "!" and n.len == 2:
result = n[1]
else:
result = n
else:
result = n
macro buildHtml*(tag, children: untyped): VNode =
expectKind children, nnkDo
var call: NimNode
if tag.kind in nnkCallKinds:
call = tag
else:
call = newCall(tag)
call.add body(children)
result = tcall2(call, nil)
when defined(debugKaraxDsl):
echo repr result
macro buildHtml*(children: untyped): VNode =
expectKind children, nnkDo
result = tcall2(body(children), nil)
when defined(debugKaraxDsl):
echo repr result

View file

@ -1,182 +0,0 @@
# Virtual DOM implementation
from dom import Event
import shash
type
VNodeKind* {.pure.} = enum
text, int, bool, thunk,
anchor,
tdiv,
table, tr, td, th, thead, tbody,
link, span, label, br, select, option,
fieldset, input, button, ul, li, section, header, footer,
h1, h2, h3, h4, h5, strong
const
toTag*: array[VNodeKind, cstring] = [
cstring"#text", "#int", "#bool", "#thunk", "A", "DIV", "TABLE", "TR", "TD",
"TH", "THEAD",
"TBODY", "LINK",
"SPAN", "LABEL", "BR", "SELECT", "OPTION", "FIELDSET", "INPUT", "BUTTON", "UL",
"LI", "SECTION", "HEADER", "FOOTER", "H1", "H2", "H3", "H4", "H5", "STRONG"
]
type
EventKind* {.pure.} = enum
onclick, ondblclick, onkeyup, onkeydown, onkeypressed, onblur, onchange, onscroll
const
toEventName*: array[EventKind, cstring] = [
cstring"click", "dblclick", "keyup", "keydown", "keypressed", "blur",
"change", "scroll"
]
type
EventHandler* = proc (ev: Event; target: VNode) {.closure.}
VKey* = int
VNode* = ref object
kind*: VNodeKind
key*: VKey
id*, class*, text*: cstring
kids: seq[VNode]
# even index: key, odd index: value; done this way for memory efficiency:
attrs: seq[cstring]
events*: seq[(EventKind, EventHandler)]
thunk*: proc (args: seq[VNode]): VNode
hash*: Hash
validHash*: bool
proc value*(n: VNode): cstring = n.text
proc `value=`*(n: VNode; v: cstring) = n.text = v
proc intValue*(n: VNode): int = n.key
proc vn*(i: int): VNode = VNode(kind: VNodeKind.int, key: i)
proc vn*(b: bool): VNode = VNode(kind: VNodeKind.int, key: ord(b))
proc vn*(x: cstring): VNode = VNode(kind: VNodeKind.text, key: -1, text: x)
proc callThunk*(n: VNode): VNode =
assert n.kind == VNodeKind.thunk
result = n.thunk(n.kids)
proc thunk*(cb: proc(args: seq[VNode]): VNode; args: varargs[VNode, vn]): VNode =
VNode(kind: VNodeKind.thunk, thunk: cb, kids: @args)
proc eq*(a, b: VNode): bool =
if a.kind != b.kind: return false
if a.id != b.id: return false
if a.class != b.class: return false
if a.kind != VNodeKind.text:
if a.kids.len != b.kids.len: return false
for i in 0..<a.kids.len:
if not eq(a.kids[i], b.kids[i]): return false
else:
if a.text != b.text: return false
if a.attrs.len != b.attrs.len: return false
for i in 0..<a.attrs.len:
if a.attrs[i] != b.attrs[i]: return false
result = true
proc setAttr*(n: VNode; key: cstring; val: cstring = "") =
if n.attrs.isNil:
n.attrs = @[key, val]
else:
for i in countup(0, n.attrs.len-2, 2):
if n.attrs[i] == key:
n.attrs[i+1] = val
return
n.attrs.add key
n.attrs.add val
proc getAttr*(n: VNode; key: cstring): cstring =
for i in countup(0, n.attrs.len-2, 2):
if n.attrs[i] == key: return n.attrs[i+1]
proc len*(x: VNode): int = x.kids.len
proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx]
proc add*(parent, kid: VNode) = parent.kids.add kid
proc newVNode*(kind: VNodeKind): VNode = VNode(kind: kind, key: -1)
proc tree*(kind: VNodeKind; kids: varargs[VNode]): VNode =
result = newVNode(kind)
for k in kids: result.add k
proc tree*(kind: VNodeKind; attrs: openarray[(cstring, cstring)];
kids: varargs[VNode]): VNode =
result = tree(kind, kids)
for a in attrs: result.setAttr(a[0], a[1])
proc text*(s: string): VNode = VNode(kind: VNodeKind.text, text: cstring(s), key: -1)
proc text*(s: cstring): VNode = VNode(kind: VNodeKind.text, text: s, key: -1)
iterator items*(n: VNode): VNode =
for i in 0..<n.kids.len: yield n.kids[i]
iterator attrs*(n: VNode): (cstring, cstring) =
for i in countup(0, n.attrs.len-2, 2):
yield (n.attrs[i], n.attrs[i+1])
proc sameAttrs*(a, b: VNode): bool =
if a.attrs.len == b.attrs.len:
result = true
for i in 0 ..< a.attrs.len:
if a.attrs[i] != b.attrs[i]: return false
proc addEventListener*(n: VNode; event: EventKind; handler: EventHandler) =
n.events.add((event, handler))
template toStringAttr(field) =
if n.field != nil:
result.add " " & astToStr(field) & " = " & $n.field
proc toString*(n: VNode; result: var string; indent: int) =
for i in 1..indent: result.add ' '
if result.len > 0: result.add '\L'
result.add "<" & $n.kind
toStringAttr(id)
toStringAttr(class)
for k, v in attrs(n):
result.add " " & $k & " = " & $v
result.add ">\L"
if n.kind == VNodeKind.text:
result.add n.text
else:
if n.text != nil:
result.add " value = "
result.add n.text
for child in items(n):
toString(child, result, indent+2)
for i in 1..indent: result.add ' '
result.add "\L</" & $n.kind & ">"
proc calcHash*(n: VNode) =
if n.validHash: return
n.validHash = true
var h: Hash = ord n.kind
if n.id != nil:
h &= "id"
h &= n.id
if n.class != nil:
h &= "class"
h &= n.class
if n.key >= 0:
h &= "k"
h &= n.key
for k, v in attrs(n):
h &= " "
h &= k
h &= "="
h &= v
if n.kind == VNodeKind.text or n.text != nil:
h &= "t"
h &= n.text
else:
for child in items(n):
calcHash(child)
h &= child.hash
n.hash = h
proc `$`*(n: VNode): cstring =
var res = ""
toString(n, res, 0)
result = cstring(res)

133
tests/blur.nim Normal file
View file

@ -0,0 +1,133 @@
import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils
type TextInput* = ref object of VComponent
value, guid: cstring
isActive: bool
onchange: proc (value: cstring)
proc render(x: VComponent): VNode =
let self = TextInput(x)
let style = style(
(StyleAttr.position, cstring"relative"),
(StyleAttr.paddingLeft, cstring"10px"),
(StyleAttr.paddingRight, cstring"5px"),
(StyleAttr.height, cstring"30px"),
(StyleAttr.lineHeight, cstring"30px"),
(StyleAttr.border, cstring"solid 1px " & (if self.isActive: cstring"red" else: cstring"black")),
(StyleAttr.fontSize, cstring"12px"),
(StyleAttr.fontWeight, cstring"600")
).merge(self.style)
let inputStyle = style.merge(style(
(StyleAttr.color, cstring"inherit"),
(StyleAttr.fontSize, cstring"inherit"),
(StyleAttr.fontWeight, cstring"inherit"),
(StyleAttr.fontFamily, cstring"inherit"),
(StyleAttr.position, cstring"absolute"),
(StyleAttr.top, cstring"0"),
(StyleAttr.left, cstring"0"),
(StyleAttr.height, cstring"100%"),
(StyleAttr.width, cstring"100%"),
(StyleAttr.border, cstring"none"),
(StyleAttr.backgroundColor, cstring"transparent"),
))
proc flip(ev: Event; n: VNode) =
self.isActive = not self.isActive
echo "flip! ", self.isActive, " id: ", self.debugId, " version ", self.version
markDirty(self)
proc onchanged(ev: Event; n: VNode) =
if self.onchange != nil:
self.onchange n.value
self.value = n.value
result = buildHtml(tdiv(style=style)):
input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip, onkeyup=onchanged)
proc changed(current, next: VComponent): bool =
let current = TextInput(current)
let next = TextInput(next)
if current.guid != next.guid:
result = true
else:
result = defaultChangedImpl(current, next)
proc update(current, next: VComponent) =
let current = TextInput(current)
let next = TextInput(next)
current.value = next.value
current.guid = next.guid
#next.isActive = current.isActive
proc newTextInput*(style: VStyle = VStyle(); guid: cstring; value: cstring = cstring"",
onchange: proc(v: cstring) = nil): TextInput =
result = newComponent(TextInput, render, changed=changed, updated=update)
result.style = style
result.value = value
result.onchange = onchange
result.guid = guid
when false:
type
Combined = ref object of VComponent
a, b: TextInput
proc renderComb(self: VComponent): VNode =
let self = Combined(self)
proc bu(ev: Event; n: VNode) =
self.a.value = ""
self.b.value = ""
markDirty(self.a)
markDirty(self.b)
result = buildHtml(tdiv(style=self.style)):
self.a
self.b
button(onclick=bu):
text "reset"
proc changed(self: VComponent): bool =
let self = Combined(self)
result = self.a.changedImpl(self.a) or self.b.changedImpl(self.b)
proc newCombined*(style: VStyle = VStyle()): Combined =
result = newComponent(Combined, renderComb, changed=changed)
result.a = newTextInput(style, "AAA")
result.b = newTextInput(style, "BBB")
var
persons: seq[cstring] = @[cstring"Karax", "Abathur", "Fenix"]
selected = -1
errmsg = cstring""
proc renderPerson(text: cstring, index: int): VNode =
proc select(ev: Event, n: VNode) =
selected = index
errmsg = ""
result = buildHtml():
tdiv:
text text
button(onClick = select)
proc createDom(): VNode =
result = buildHtml(tdiv):
tdiv:
for index, text in persons.pairs:
renderPerson(text, index)
tdiv:
newTextInput(VStyle(), &selected, if selected >= 0: persons[selected] else: "", proc (v: cstring) =
if v.len > 0:
if selected >= 0: persons[selected] = v
errmsg = ""
else:
errmsg = "name must not be empty"
)
tdiv:
text errmsg
setRenderer createDom

18
tests/compiler_tests.nim Normal file
View file

@ -0,0 +1,18 @@
include ../karax/prelude
block:
type
TestEnum {.pure.} = enum
ValA, ValB
var e = TestEnum.ValA
proc renderDom(): VNode {.used.} =
result = buildHtml():
tdiv():
case e
of TestEnum.ValA:
tdiv: text "A"
of TestEnum.ValB:
tdiv: text "B"

128
tests/components.nim Normal file
View file

@ -0,0 +1,128 @@
import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils
type TextInput* = ref object of VComponent
value: cstring
isActive: bool
proc render(x: VComponent): VNode =
let self = TextInput(x)
let style = style(
(StyleAttr.position, cstring"relative"),
(StyleAttr.paddingLeft, cstring"10px"),
(StyleAttr.paddingRight, cstring"5px"),
(StyleAttr.height, cstring"30px"),
(StyleAttr.lineHeight, cstring"30px"),
(StyleAttr.border, cstring"solid 1px " & (if self.isActive: cstring"red" else: cstring"black")),
(StyleAttr.fontSize, cstring"12px"),
(StyleAttr.fontWeight, cstring"600")
).merge(self.style)
let inputStyle = style.merge(style(
(StyleAttr.color, cstring"inherit"),
(StyleAttr.fontSize, cstring"inherit"),
(StyleAttr.fontWeight, cstring"inherit"),
(StyleAttr.fontFamily, cstring"inherit"),
(StyleAttr.position, cstring"absolute"),
(StyleAttr.top, cstring"0"),
(StyleAttr.left, cstring"0"),
(StyleAttr.height, cstring"100%"),
(StyleAttr.width, cstring"100%"),
(StyleAttr.border, cstring"none"),
(StyleAttr.backgroundColor, cstring"transparent"),
))
proc flip(ev: Event; n: VNode) =
self.isActive = not self.isActive
echo "flip! ", self.isActive, " id: ", self.debugId, " version ", self.version
markDirty(self)
result = buildHtml(tdiv(style=style)):
input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip,
events=self.events)
proc update(current, next: VComponent) =
let current = TextInput(current)
let next = TextInput(next)
current.value = next.value
current.key = next.key
proc changed(current, next: VComponent): bool = true
proc newTextInput*(style: VStyle = VStyle(); key: cstring;
value = cstring""): TextInput =
result = newComponent(TextInput, render, changed=changed, updated=update)
result.style = style
result.value = value
#result.key = key
when false:
type
Combined = ref object of VComponent
a, b: TextInput
proc renderComb(self: VComponent): VNode =
let self = Combined(self)
proc bu(ev: Event; n: VNode) =
self.a.value = ""
self.b.value = ""
markDirty(self.a)
markDirty(self.b)
result = buildHtml(tdiv(style=self.style)):
self.a
self.b
button(onclick=bu):
text "reset"
proc changed(self: VComponent): bool =
let self = Combined(self)
result = self.a.changedImpl(self.a) or self.b.changedImpl(self.b)
proc newCombined*(style: VStyle = VStyle()): Combined =
result = newComponent(Combined, renderComb, changed=changed)
result.a = newTextInput(style, "AAA")
result.b = newTextInput(style, "BBB")
var
persons: seq[cstring] = @[cstring"Karax", "Abathur", "Fenix"]
errmsg = cstring""
proc renderPerson(text: cstring, index: int): VNode =
result = buildHtml(tdiv):
newTextInput(VStyle(), &index, text):
proc onkeyuplater(ev: Event; n: VNode) =
let v = n.value
if v.len > 0:
persons[index] = v
errmsg = ""
else:
errmsg = "name must not be empty"
button:
proc onclick(ev: Event; n: VNode) =
persons.delete(index)
errmsg = ""
echo persons
text "(x)"
proc createDom(): VNode =
result = buildHtml(tdiv):
tdiv:
for index, text in persons.pairs:
renderPerson(text, index)
tdiv:
newTextInput(VStyle(), &persons.len, ""):
proc onkeyupenter(ev: Event; n: VNode) =
let v = n.value
if v.len > 0:
persons.add v
errmsg = ""
else:
errmsg = "name must not be empty"
tdiv:
text errmsg
setRenderer createDom

View file

@ -3,14 +3,13 @@
<html>
<head>
<title>Diff dom tests</title>
<link rel="stylesheet" href="style.css">
</head>
<body id="body">
<div id="ROOT" />
<script type="text/javascript" src="nimcache/diffDomTests.js"></script>
<script type="text/javascript" src="diffDomTests.js"></script>
<script type="text/javascript">
onload(window.location.pathname.split('/')[1])
</script>

View file

@ -1,183 +1,107 @@
import vdom, times, karax, karaxdsl, jdict, jstrutils, parseutils, sequtils
import kdom, vdom, times, karax, karaxdsl, jdict, jstrutils, parseutils, sequtils
var
entries: seq[cstring]
results: seq[cstring]
timeout : Timeout
entries: seq[cstring]
results: seq[cstring]
proc reset() =
results.add cstring"reset started"
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5") ]
redraw()
results.add cstring"reset finished"
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
redrawSync()
proc checkOrder(order : seq[int]): bool =
var ul = getElementById("ul")
if ul == nil or len(ul.children) != len(order):
return false
var pos = 0
for child in ul.children:
if child.id != $order[pos]:
return false
inc pos
return true
proc checkOrder(order: seq[int]): bool =
var ul = getElementById("ul")
if ul == nil or len(ul.children) != len(order):
kout ul, len(order)
return false
var pos = 0
for child in ul.children:
if child.id != $order[pos]:
kout pos
return false
inc pos
return true
proc check1() =
let result = checkOrder(@[0, 1, 2, 3, 4, 7, 5])
if result:
results.add cstring"test1 - OK"
else:
results.add cstring"test1 - FAIL"
proc check(name: cstring; order: seq[int]) =
let result = checkOrder(order)
results.add name & (if result: cstring" - OK" else: cstring" -FAIL")
# result: 0 1 2 3 4 7 5
proc test1() =
results.add cstring"test1 started"
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5") ]
entries.insert(cstring("7"), 5)
redraw()
timeout = setTimeout(check1, 20)
results.add cstring"test1 started"
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
entries.insert(cstring("7"), 5)
redrawSync()
check("test1", @[0, 1, 2, 3, 4, 7, 5])
proc check2() =
let result = checkOrder(@[8, 0, 1, 2, 3, 4, 7, 5])
if result:
results.add cstring"test1 - OK"
else:
results.add cstring"test2 - FAIL"
# result: 8 0 1 2 3 4 7 5
proc test2() =
results.add cstring"test2 started"
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5") ]
entries.insert(cstring("7"), 5)
entries.insert(cstring("8"), 0)
redraw()
timeout = setTimeout(check2, 20)
results.add cstring"test2 started"
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
entries.insert(cstring("7"), 5)
entries.insert(cstring("8"), 0)
redrawSync()
check("test2", @[8, 0, 1, 2, 3, 4, 7, 5])
proc check3() =
let result = checkOrder(@[2, 3, 4, 1])
if result:
results.add cstring"test3 - OK"
else:
results.add cstring"test3 - FAIL"
# result: 2 3 4 1
proc test3() =
results.add cstring"test3 started"
entries = @[cstring("2"), cstring("3"), cstring("4"), cstring("1") ]
redraw()
timeout = setTimeout(check3, 20)
results.add cstring"test3 started"
entries = @[cstring("2"), cstring("3"), cstring("4"), cstring("1")]
redrawSync()
check("test3", @[2, 3, 4, 1])
proc check4() =
let result = checkOrder(@[5, 6, 7, 8])
if result:
results.add cstring"test4 - OK"
else:
results.add cstring"test4 - FAIL"
# result: 5 6 7 8
proc test4() =
results.add cstring"test4 started"
entries = @[cstring("5"), cstring("6"), cstring("7"), cstring("8") ]
redraw()
timeout = setTimeout(check4, 20)
results.add cstring"test4 started"
entries = @[cstring("5"), cstring("6"), cstring("7"), cstring("8") ]
redrawSync()
check("test4", @[5, 6, 7, 8])
proc check5() =
let result = checkOrder(@[0, 1, 3, 5, 4, 5])
if result:
results.add cstring"test5 - OK"
else:
results.add cstring"test5 - FAIL"
# result: 0 1 3 5 4 5
proc test5() =
results.add cstring"test5 started"
entries = @[cstring("0"), cstring("1"), cstring("3"), cstring("5"), cstring("4"), cstring("5") ]
redraw()
timeout = setTimeout(check5, 20)
results.add cstring"test5 started"
entries = @[cstring("0"), cstring("1"), cstring("3"), cstring("5"), cstring("4"), cstring("5")]
redrawSync()
check("test 5", @[0, 1, 3, 5, 4, 5])
proc check6() =
let result = checkOrder(@[])
if result:
results.add cstring"test6 - OK"
else:
results.add cstring"test6 - FAIL"
# result: empty
proc test6() =
results.add cstring"test6 started"
entries = @[]
redraw()
timeout = setTimeout(check6, 20)
proc check7() =
let result = checkOrder(@[2])
if result:
results.add cstring"test7 - OK"
else:
results.add cstring"test7 - FAIL"
redraw()
results.add cstring"test6 started"
entries = @[]
redrawSync()
check("test 6", @[])
# result: 2
proc test7() =
results.add cstring"test7 started"
entries = @[cstring("2")]
redraw()
timeout = setTimeout(check7, 20)
results.add cstring"test7 started"
entries = @[cstring("2")]
redrawSync()
check("test 7", @[2])
proc createEntry(id: int): VNode =
result = buildHtml():
button(id="" & $id):
text $id
text $id
proc createDom(): VNode =
result = buildHtml(tdiv()):
ul(id="ul"):
for e in entries:
createEntry(parseInt(e))
for r in results:
tdiv:
text r
result = buildHtml(tdiv()):
ul(id="ul"):
for e in entries:
createEntry(parseInt(e))
for r in results:
tdiv:
text r
proc onload() =
for i in 0..5: # 0_000:
entries.add(cstring($i))
test1()
reset()
test2()
reset()
test3()
reset()
test4()
reset()
test5()
reset()
test6()
reset()
test7()
setRenderer createDom
proc onload(session: cstring) {.exportc.} =
for i in 0..5: # 0_000:
entries.add(cstring($i))
init()
var dtReset = 100
var dtTest = 500
var t = dtReset
timeout = setTimeout(test1, t)
t += dtTest
timeout = setTimeout(reset, t)
t += dtReset
timeout = setTimeout(test2, t)
t += dtTest
timeout = setTimeout(reset, t)
t += dtReset
timeout = setTimeout(test3, t)
t += dtTest
timeout = setTimeout(reset, t)
t += dtReset
timeout = setTimeout(test4, t)
t += dtTest
timeout = setTimeout(reset, t)
t += dtReset
timeout = setTimeout(test5, t)
t += dtTest
timeout = setTimeout(reset, t)
t += dtReset
timeout = setTimeout(test6, t)
t += dtTest
timeout = setTimeout(reset, t)
t += dtReset
timeout = setTimeout(test7, t)
onload()

185
tests/difftest.nim Normal file
View file

@ -0,0 +1,185 @@
include "../karax/karax"
import "../karax/karaxdsl"
proc hasDom(n: Vnode) =
if n.kind in {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}:
discard
else:
doAssert n.dom != nil
for i in 0..<n.len: hasDom(n[i])
proc shortRepr(n: VNode): string =
if n == nil:
result = "nil"
elif n.kind == VNodeKind.text:
result = $n.text
else:
result = $n.kind
for i in 0..<n.len:
result &= " " & shortRepr(n[i])
var err = 0
proc doDiff(a, b: VNode; expected: varargs[string]) =
diff(b, a, nil, vnodeToDom(a, kxi), kxi)
var j = 0
for i in 0..<kxi.patchLen:
if kxi.patches[i].k != pkSame:
let n = if kxi.patches[i].k == pkDetach: kxi.patches[i].oldNode else: kxi.patches[i].newNode
let p = $kxi.patches[i].k & " " & shortRepr(n)
if j >= expected.len:
echo "patches differ; expected nothing but got: ", p
inc err
elif p != expected[j]:
echo "patches differ; expected ", expected[i], " but got: ", p
inc err
inc j
#hasDom(kxi.currentTree)
kxi.patchLen = 0
proc testAppend() =
let a = buildHtml(tdiv):
ul:
li: text "A"
li: text "B"
let b = buildHtml(tdiv):
ul:
li: text "A"
li: text "B"
li:
tdiv:
text "C"
doDiff(a, b, "pkAppend li div C")
proc testInsert() =
let a = buildHtml(tdiv):
ul:
li: text "A"
li:
button:
text "C"
let b = buildHtml(tdiv):
ul:
li: text "A"
li: text "B"
li:
button:
text "C"
doDiff(a, b, "pkInsertBefore li B")
proc testInsert2() =
let a = buildHtml(tdiv):
tdiv:
tdiv:
ul:
li: text "A"
li:
button:
text "D"
let b = buildHtml(tdiv):
tdiv:
tdiv:
ul:
li: text "A"
li: text "B"
li: text "C"
li:
button:
text "D"
doDiff(a, b, "pkInsertBefore li B", "pkInsertBefore li C")
proc testDelete() =
let a = buildHtml(tdiv):
ul:
li: text "A"
li: text "B"
li: text "C"
let b = buildHtml(tdiv):
ul:
discard
doDiff(a, b, "pkDetach li A", "pkRemove nil",
"pkDetach li B", "pkRemove nil",
"pkDetach li C", "pkRemove nil")
proc testDeleteMiddle() =
let a = buildHtml(tdiv):
ul:
li:
tdiv: text "A"
li:
tdiv: text "B"
li:
tdiv: text "C"
li:
tdiv: text "D"
li:
tdiv: text "E"
li:
tdiv: text "F"
li:
tdiv: text "G"
li:
button:
tdiv: text "H"
let b = buildHtml(tdiv):
ul:
li:
tdiv: text "A"
li:
tdiv: text "B"
li:
tdiv: text "C"
li:
tdiv: text "D"
li:
tdiv: text "E"
li:
tdiv: text "F"
li:
button:
tdiv: text "H"
doDiff(a, b, "pkDetach li div G", "pkRemove nil")
proc createEntry(id: cstring): VNode =
result = buildHtml():
button(id="" & id):
text id
proc createEntries(entries: seq[cstring]): VNode =
result = buildHtml(tdiv()):
ul(id="ul"):
for e in entries:
createEntry(e)
for r in entries:
tdiv:
text r
proc testWild() =
var entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring"7", cstring("5")]
let a = createEntries(entries)
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
let b = createEntries(entries)
doDiff(a, b, "pkDetach button 7", "pkRemove nil", "pkDetach div 7", "pkRemove nil")
proc testWildInsert() =
var entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
let a = createEntries(entries)
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring"7", cstring("5")]
let b = createEntries(entries)
doDiff(a, b, "pkInsertBefore button 7", "pkInsertBefore div 7")
kxi = KaraxInstance(rootId: cstring"ROOT", renderer: proc (data: RouterData): VNode = discard,
byId: newJDict[cstring, VNode]())
testAppend()
testInsert()
testInsert2()
testDelete()
testWild()
testWildInsert()
testDeleteMiddle()
if err == 0:
echo "Success"
else:
quit 1

33
tests/domEventTests.css Normal file
View file

@ -0,0 +1,33 @@
#dragDiv {
background-color: #0d1224;
width: 500px;
height: 100px;
position: absolute;
top: 500px;
left: 200px;
text-align: center;
cursor: move;
color: rgb(60, 201, 163);
}
#results {
font-family: monospace;
font-size: 10pt;
width: 100%;
height: 400px;
overflow: auto;
background-color: #000000;
color: forestgreen;
}
#test10input1, #test10input2, #test11h3, #test12input {
visibility: hidden;
}
#test11h3 {
color: gold;
}
body {
background-color: dimgrey;
}

13
tests/domEventTests.html Normal file
View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>DOM Event Tests</title>
<link href="domEventTests.css" rel="stylesheet" />
<meta charset="UTF-8" />
<script src="jquery-3.2.1.js" type="text/javascript"></script>
</head>
<body id="body">
<div id="ROOT"></div>
<script src="domEventTests.js" type="text/javascript"></script>
</body>
</html>

624
tests/domEventTests.nim Normal file
View file

@ -0,0 +1,624 @@
import times, jstrutils, strutils, kdom, karax, karaxdsl, vdom, tables
var
results: seq[cstring]
dragEventsCompleted = initTable[DragEventTypes, bool]()
testRunning: array[13, bool]
testCompleted: array[13, bool]
test2ImageDrop = false
dblClickCount = 0
test6ClickCount = 0
test8Failure = false
test10inputFocusCount = 0
test12Failure = false
proc getLogText(text: string): cstring =
result = "[$#]: $#" % [format(getLocalTime(getTime()), "HH:mm:ss"), $text]
proc log(text: string, clear: bool = false) =
if clear:
results = @[]
results.add getLogText(text)
echo text
redrawSync()
# Scroll to top after adding text using JQuery
discard window.setTimeout("$('#results').animate({ scrollTop: 0 }, 100);", 100)
proc check(cond: bool, msg: string) =
if not cond:
log "FAIL: " & msg
doAssert(cond, msg)
proc check(cond: bool, msg: string, warn: bool): bool =
if not cond:
if warn:
log "WARNING: " & msg
else:
log "FAIL: " & msg
doAssert(cond, msg)
result = cond
proc dragDivDragStart(ev: kdom.Event) =
dragEventsCompleted[DragStart] = true
var dgev = cast[DragEvent](ev)
var dragDiv = document.getElementById("dragDiv")
if dragDiv != nil:
var style = window.getComputedStyle(dgev.target)
var dataX = $(parseInt(style.left) - dgev.clientX)
var dataY = $(parseInt(style.top) - dgev.clientY)
dgev.dataTransfer.setData("text/plain", dataX & "," & dataY)
var img = (ImageElement)document.createElement("img")
img.src ="https://nim-lang.org/assets/img/logo.svg"
dgev.dataTransfer.setDragImage(img, 0, 0)
proc dragDivDragOver(ev: kdom.Event) =
dragEventsCompleted[DragOver] = true
ev.preventDefault()
proc checkAllTests() =
var allTestsPassed = true
for a in testCompleted:
if not a:
allTestsPassed = false
break
if allTestsPassed:
log "All tests completed successfully!"
else:
log "Some tests failed. Check the log."
proc completeTest13() =
testRunning[12] = false
testCompleted[12] = true
log "Test 13 completed successfully."
checkAllTests()
proc test13() =
testRunning[12] = true
log "Test 13 started."
log "This test verifies KeyboardEvent getModifierState functionality."
log "To continue, press the left CTRL key."
proc test12focus1(ev: kdom.Event) =
ev.stopImmediatePropagation()
proc test12focus2(ev: kdom.Event) =
test12Failure = true
check false, "Unexpected listener invocation."
proc test12focus3(ev: kdom.Event) =
test12Failure = true
check false, "Unexpected listener invocation."
proc completeTest12() =
if not test12Failure:
var elmt = document.getElementById("test12input")
elmt.style.visibility = "hidden"
testRunning[11] = false
testCompleted[11] = true
log "Test 12 completed successfully."
test13()
proc test12() =
testRunning[11] = true
log "Test 12 started."
log "This test verifies stopImmediatePropagation functionality."
var elmt = document.getElementById("test12input")
elmt.style.visibility = "visible"
elmt.addEventListener($Focus, test12focus1)
elmt.addEventListener($Focus, test12focus2)
elmt.addEventListener($Focus, test12focus3)
elmt.focus()
discard setTimeout(completeTest12, 500)
proc completeTest11() =
document.querySelector("h3").style.visibility = "hidden"
testRunning[10] = false
testCompleted[10] = true
log "Test 11 completed."
test12()
proc test11() =
testRunning[10] = true
log "Test 11 started."
log "This test checks querySelector and querySelectorAll."
document.getElementById("test11h3").style.visibility = "visible"
var elmt = document.querySelector("h3")
check elmt != nil, "querySelector returned nil."
log "text: " & $elmt.innerHTML
check $elmt.innerHTML == "test 11", "Unexpected h3 value."
var elmts = document.querySelectorAll("h3")
log "elmts len: " & $elmts.len
check elmts.len == 1, "Unexpected elmts len."
log "text: " & $elmts[0].innerHTML
check $elmts[0].innerHTML == "test 11", "Unexpected h3 value."
discard setTimeout(completeTest11, 500)
proc completeTest10() =
testRunning[9] = false
testCompleted[9] = true
var test10input1 = document.getElementById("test10input1")
var test10input2 = document.getElementById("test10input2")
test10input1.style.visibility = "hidden"
test10input2.style.visibility = "hidden"
log "Test 10 completed."
test11()
proc test10inputOnfocus(ev: kdom.Event) =
if testRunning[9]:
inc test10inputFocusCount
if test10inputFocusCount > 3:
discard setTimeout(completeTest10, 500)
proc test10() =
log "Test 10 started."
log "This test checks element.focus()."
testRunning[9] = true
var test10input1 = document.getElementById("test10input1")
var test10input2 = document.getElementById("test10input2")
test10input1.style.visibility = "visible"
test10input2.style.visibility = "visible"
test10input1.addEventListener($DomEvent.Focus, test10inputOnfocus)
test10input2.addEventListener($DomEvent.Focus, test10inputOnfocus)
test10input1.focus()
test10input2.focus()
test10input1.focus()
test10input2.focus()
proc completeTest9() =
var test9Value = $document.getElementById("test9value").value
doAssert(test9Value == "[test9]", "Unexpected test9 value.")
testRunning[8] = false
testCompleted[8] = true
test10()
proc test9() =
log "Test 9 started."
log "This tests the setTimeout(w: Window, code: cstring, pause: int) proc."
testRunning[8] = true
discard window.setTimeout("document.getElementById('test9value').value = '[test9]';", 500)
discard window.setTimeout(completeTest9, 1000)
proc completeTest8() =
if not test8Failure:
log "Test 8 completed successfully."
testRunning[7] = false
testCompleted[7] = true
test9()
else:
log "Test 8 failed."
proc failTest8() =
test8Failure = true
check(false, "ClearTimeout failed.")
proc test8() =
log "Test 8 started."
log "This test checks that clearTimeout() works correctly."
testRunning[7] = true
var timeout = setTimeout(failTest8, 1000)
clearTimeout(timeout)
discard setTimeout(completeTest8, 1200)
proc test7() =
log "Test 7 started."
log "This test ensures that mouse events correctly pass button and key modifier information."
log "To continue, right-click while holding SHIFT."
testRunning[6] = true
# A mouseMove handler is needed (even if empty) so mouseDown will have a value for movementX and movementY
proc mouseMove(ev: kdom.Event) =
discard
proc mouseDown(ev: kdom.Event) =
var me = (MouseEvent)ev
if testRunning[5]:
log "[ME] button: " & $me.button
log "[ME] buttons: " & $me.buttons
log "[ME] altKey: " & $me.altKey
log "[ME] ctrlKey: " & $me.ctrlKey
log "[ME] metaKey: " & $me.metaKey
log "[ME] shiftKey: " & $me.shiftKey
log "[ME] clientX: " & $me.clientX
check me.clientX > 0, "Unexpected clientX value."
log "[ME] clientY: " & $me.clientY
check me.clientY > 0, "Unexpected clientY value."
log "[ME] movementX: " & $me.movementX
check me.movementX > 0, "Unexpected movementX value."
log "[ME] movementY: " & $me.movementY
check me.movementY > 0, "Unexpected movementY value."
log "[ME] offsetX: " & $me.offsetX
check me.offsetX > 0, "Unexpected offsetX value."
log "[ME] offsetY: " & $me.offsetY
check me.offsetY > 0, "Unexpected offsetY value."
log "[ME] pageX: " & $me.pageX
check me.pageX > 0, "Unexpected pageX value."
log "[ME] pageY: " & $me.pageY
check me.pageY > 0, "Unexpected pageY value."
log "[ME] relatedTarget == nil: " & $(me.relatedTarget == nil)
log "[ME] screenX: " & $me.screenX
check me.screenX > 0, "Unexpected screenX value."
log "[ME] screenY: " & $me.screenY
check me.screenY > 0, "Unexpected screenY value."
log "[ME] x: " & $me.x
check me.x > 0, "Unexpected x value."
log "[ME] y: " & $me.y
check me.y > 0, "Unexpected y value."
if dblClickCount == 1:
inc test6ClickCount
if test6ClickCount > 1:
testRunning[5] = false
testCompleted[5] = true
log "Test 6 completed successfully."
test7()
elif testRunning[6]:
log "[ME] buttons: " & $me.buttons
var passed = check(me.buttons == (int)SecondaryButton, "Unexpected buttons value.", true)
log "[ME] shiftKey: " & $me.shiftKey
passed = passed and check(me.shiftKey, "Unexpected shiftKey value.", true)
var isShiftPressed = me.getModifierState($KeyboardEventKey.Shift)
log "[ME] getModifierState shift result: " & $isShiftPressed
passed = passed and check(isShiftPressed, "Unexpected getModifierState shift result.", true)
if passed:
log "Test 7 completed successfully."
testRunning[6] = false
testCompleted[6] = true
ev.preventDefault()
if testCompleted[6]:
test8()
proc dblClick(ev: kdom.Event) =
inc dblClickCount
check dblClickCount == 1, "Unexpected double-click event invocation."
log "Try double-clicking again."
proc test6() =
log "Test 6 started."
log "This test validates that addEventListener can be called by passing AddEventListenerOptions, and the options work correctly."
log "To continue, try double-clicking the mouse."
var options = AddEventListenerOptions(once:true)
document.addEventListener($DomEvent.DblClick, dblClick, options)
testRunning[5] = true
proc test5() =
log "Test 5 started."
log "This test validates the newly added properties: window.screen.width, window.scren.height, element.clientWidth, and element.clientHeight."
log "window.screen.width: " & $window.screen.width
check(window.screen.width > 0, "Unexpected value for window.screen.width.")
log "window.screen.height: " & $window.screen.height
check(window.screen.height > 0, "Unexpected value for window.screen.height.")
var elem = document.getElementById("results")
log "results div clientWidth: " & $elem.clientWidth
check(elem.clientWidth > 0, "Unexpected value for clientWidth.")
log "results div clientHeight: " & $elem.clientHeight
check(elem.clientHeight > 0, "Unexpected value for clientHeight.")
testRunning[4] = false
testCompleted[4] = true
log "Test 5 completed successfully."
test6()
proc keyDown(ev: kdom.Event) =
var kbev = ((KeyboardEvent)ev)
if testRunning[2]:
log "[KB] altKey: " & $kbev.altKey
log "[KB] ctrlKey: " & $kbev.ctrlKey
log "[KB] metaKey: " & $kbev.metaKey
log "[KB] shiftKey: " & $kbev.shiftKey
log "[KB] code: " & $kbev.code
log "[KB] isComposing: " & $kbev.isComposing
log "[KB] key: " & $kbev.key
log "[KB] keyCode: " & $kbev.keyCode
log "[KB] location: " & $kbev.location
log "Test 3 completed successfully."
testRunning[2] = false
testCompleted[2] = true
testRunning[3] = true
log "Test 4 started."
log "This test ensures the child events have access to base event settings."
log "[KB] event.bubbles: " & $kbev.bubbles
check kbev.bubbles, "Unexpected event.bubbles value."
log "[KB] event.cancelBubble: " & $kbev.cancelBubble
check kbev.cancelBubble == false, "Unexpected event.cancelBubble value."
log "[KB] event.cancelable: " & $kbev.cancelable
check kbev.cancelable, "Unexpected event.cancelable value."
log "[KB] event.composed: " & $kbev.composed
check kbev.composed, "Unexpected event.composed value."
log "[KB] event.currentTarget.nodeName: " & $kbev.currentTarget.nodeName
check kbev.currentTarget.nodeName == "#document", "Unexpected event.currentTarget.nodeName value."
log "[KB] event.defaultPrevented: " & $kbev.defaultPrevented
check kbev.defaultPrevented == false, "Unexpected event.defaultPrevented value."
log "[KB] event.eventPhase: " & $kbev.eventPhase
check EventPhase(kbev.eventPhase) == BubblingPhase, "Unexpected event.eventPhase value."
log "[KB] event.isTrusted: " & $kbev.isTrusted
check kbev.isTrusted, "Unexpected event.isTrusted value."
log "[UI] kbev.detail: " & $kbev.detail
log "[UI] kbev.view.outerHeight: " & $kbev.view.outerHeight
check kbev.view.outerHeight > 0, "Unexpected view.outerHeight value."
log "[UI] kbev.view.outerWidth: " & $kbev.view.outerWidth
check kbev.view.outerWidth > 0, "Unexpected view.outerWidth value."
testRunning[3] = false
testCompleted[3] = true
log "Test 4 completed."
test5()
elif testRunning[12]:
if kbev.code == "ControlLeft":
log "[KB] altKey: " & $kbev.altKey
log "[KB] ctrlKey: " & $kbev.ctrlKey
log "[KB] metaKey: " & $kbev.metaKey
log "[KB] shiftKey: " & $kbev.shiftKey
log "[KB] code: " & $kbev.code
log "[KB] isComposing: " & $kbev.isComposing
log "[KB] key: " & $kbev.key
log "[KB] keyCode: " & $kbev.keyCode
log "[KB] location: " & $kbev.location
var isControl = kbev.getModifierState($KeyboardEventKey.Control)
log "[KB] getModifierState result: " & $isControl
check isControl, "Unexpected getModifierState return value."
completeTest13()
proc test3() =
log "Test 3 started."
log "This test validates KeyboardEvent handling."
log "Click on the browser to focus, and then press any key."
testRunning[2] = true
proc dragDivDrop(ev: kdom.Event) =
dragEventsCompleted[Drop] = true
var dgev = cast[DragEvent](ev)
var dragDiv = document.getElementById("dragDiv")
var data = $dgev.dataTransfer.getData("text/plain")
var dt = dgev.dataTransfer
if not test2ImageDrop:
var offset = data.split(",")
var newLeft = dgev.clientX + parseInt(offset[0])
var newTop = dgev.clientY + parseInt(offset[1])
if newLeft < 0: newLeft = 0
if newTop < 0: newTop = 0
if newLeft + dragDiv.clientWidth > window.outerWidth: newLeft = window.outerWidth - dragDiv.clientWidth
if newTop + dragDiv.clientHeight > window.outerHeight: newTop = window.outerHeight - dragDiv.clientHeight
dragDiv.style.left = $newLeft & "px"
dragDiv.style.top = $newTop & "px"
if testRunning[0]:
log "[drop] Drop coords: $#, $#" % [$newLeft, $newTop]
if testCompleted[0] and not testCompleted[1] and not testRunning[1]:
testRunning[1] = true
log "Test 2 started."
log "This test validates the DataTransfer and DataTransferItem objects passed as part of the drag events."
var isChrome = ($navigator.userAgent).contains("Chrome")
log "[DT] effectAllowed: " & $dt.effectAllowed
var expectedEffectAllowed = $Uninitialized
if isChrome:
expectedEffectAllowed = if test2ImageDrop: $DataTransferEffectAllowed.Copy else: $DataTransferEffectAllowed.All
check(dt.effectAllowed == $expectedEffectAllowed, "Unexpected effectAllowed value.")
if not test2ImageDrop:
log "[DT] dropEffect: " & $dt.dropEffect
var expectedDropEffect = if isChrome: $DataTransferDropEffect.None else: $DataTransferDropEffect.Move
check(dt.dropEffect == $expectedDropEffect, "Unexpected dropEffect value.")
var expectedFilesLen = if test2ImageDrop: 1 else: 0
log "[DT] files len: " & $dt.files.len
check(dt.files.len == expectedFilesLen, "Unexpected files length.")
var expectedItemsLen = 1
log "[DT] items len: " & $dt.items.len
check(dt.items.len == expectedItemsLen, "Unexpected items length.")
var expectedItemKind = if test2ImageDrop: $DataTransferItemKind.File else: $DataTransferItemKind.String
log "[DT] items[0].kind: " & $dt.items[0].kind
check(dt.items[0].kind == expectedItemKind, "Unexpected item kind.")
var expectedItemType = if test2ImageDrop: "image/png" else: "text/plain"
log "[DT] items[0].type: " & $dt.items[0].`type`
check(dt.items[0].`type` == expectedItemType, "Unexpected item type.")
var expectedTypesLen = if not isChrome and test2ImageDrop: 2 else: 1
log "[DT] types len: " & $dt.types.len
check(dt.types.len == expectedTypesLen, "Unexpected types length.")
if test2ImageDrop:
log "[DT] types[0]: " & $dt.types[0]
if isChrome:
check(dt.types[0] == "Files", "Unexpected type value.")
else:
log "[DT] types[1]: " & $dt.types[1]
check(dt.types[0] == "application/x-moz-file", "Unexpected type value.")
check(dt.types[1] == "Files", "Unexpected type value.")
if test2ImageDrop:
test2ImageDrop = false
# TODO: Determine why getAsString() does invoke callback
# proc getAsString*(dti: DataTransferItem, callback: proc(data: cstring))
var jfile = dt.items[0].getAsFile()
log "[DT] file name: " & $jfile.name
check(jfile.name.len > 0, "Unexpected file name length.")
testCompleted[1] = true
testRunning[1] = false
log "Test 2 completed successfully."
test3()
ev.preventDefault()
return
document.getElementById("dragDiv").style.visibility = "hidden"
log "To continue, drag a .PNG file (file type matters) anywhere on this page."
log "NOTE: If the browser redirects to the image, then consider this test as failed."
test2ImageDrop = true
testRunning[1] = false
ev.preventDefault()
proc dragDivDrag(ev: kdom.Event) =
dragEventsCompleted[Drag] = true
ev.preventDefault()
proc dragDivDragLeave(ev: kdom.Event) =
dragEventsCompleted[DragLeave] = true
if testRunning[0]:
log "[dragLeave]"
ev.preventDefault()
proc dragDivDragExit(ev: kdom.Event) =
dragEventsCompleted[DragExit] = true
if testRunning[0]:
log "[dragExit]"
ev.preventDefault()
proc dragDivDragEnd(ev: kdom.Event) =
dragEventsCompleted[DragEnd] = true
if testRunning[0]:
log "[dragEnd]"
ev.preventDefault()
proc dragDivDragEnter(ev: kdom.Event) =
dragEventsCompleted[DragEnter] = true
if testRunning[0]:
log "[dragEnter]"
ev.preventDefault()
proc initDragEvents() =
log "Drag event initialization started."
var dragDiv = document.getElementById("dragDiv")
document.addEventListener($DragEventTypes.DragOver, dragDivDragOver, false)
document.addEventListener($DragEventTypes.Drop, dragDivDrop, false)
document.addEventListener($DragEventTypes.Drag, dragDivDrag, false)
dragDiv.addEventListener($DragEventTypes.DragStart, dragDivDragStart, false)
dragDiv.addEventListener($DragEventTypes.DragExit, dragDivDragExit, false)
dragDiv.addEventListener($DragEventTypes.DragLeave, dragDivDragLeave, false)
dragDiv.addEventListener($DragEventTypes.DragEnd, dragDivDragEnd, false)
dragDiv.addEventListener($DragEventTypes.DragEnter, dragDivDragEnter, false)
log "Drag event initialization complete."
log "Drag the div to continue the test. Make sure you drag the div over another element."
log "Then click the \"Check test 1 status\" button."
proc initMouseEvents() =
log "Mouse event initialization started."
document.addEventListener($DomEvent.MouseDown, mouseDown, false)
document.addEventListener($DomEvent.MouseMove, mouseMove, false)
log "Mouse event initialization complete."
proc initKbEvents() =
log "KB event initialization started."
document.addEventListener($DomEvent.KeyDown, keyDown, false)
log "KB event initialization complete."
proc startTests() =
log "[Disclaimer: these tests have been tested successfully on Linux with Firefox and Chromium. Your results may vary.]"
log "Note: these tests require some manual steps, which will be shown here."
log "Note: new entries appear in this text area at the top, so read in this direction."
log " | | | | | | | | | | | | | | | | | | | | | |"
log " / \\ / \\ / \\ / \\ / \\ / \\ / \\ / \\ / \\ / \\ / \\"
log "Test 1 started."
log "This test checks that drag events are fired."
testRunning[0] = true
for evt in [Drag, DragEnd, DragEnter, DragExit, DragLeave, DragOver, DragStart, Drop]:
dragEventsCompleted.add(evt, false)
initKbEvents()
initMouseEvents()
initDragEvents()
proc checkTest1Status() =
var evtsCompleted = true
# Chrome doesn't seem to send DragExit, so ignore this
if (($navigator.userAgent).contains("Chrome")):
dragEventsCompleted[DragExit] = true
for evt, completed in dragEventsCompleted:
if not completed:
evtsCompleted = false
break
if evtsCompleted:
testRunning[0] = false
log "Test 1 completed successfully. Drag the div anywhere to start the next test."
testCompleted[0] = true
document.getElementById("checkTest1").style.visibility = "hidden"
else:
log "Test 1 has not completed yet. Make sure you drag the div over another element."
log "Status:"
for evt, completed in dragEventsCompleted:
log $evt & ": " & $completed
proc createDom(): VNode =
result = buildHtml(tdiv()):
tdiv(id="dragDiv", draggable="true"):
tdiv(class="divcontent"):
text "draggable div"
tdiv(id="results"):
for i in countdown(results.len - 1, 0):
tdiv:
text $results[i]
tdiv(id="checkTest1"):
button(onclick=checkTest1Status):
text "Check test 1 status"
input(`type`="hidden", id="test9value")
input(`type`="text", id="test10input1")
input(`type`="text", id="test10input2")
h3(id="test11h3"):
text "test 11"
input(`type`="text", id="test12input")
proc onload() =
startTests()
setRenderer createDom
onload()

10253
tests/jquery-3.2.1.js vendored Normal file

File diff suppressed because it is too large Load diff

18
tests/lists.nim Normal file
View file

@ -0,0 +1,18 @@
import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils
var contents: seq[cstring] = @[]
proc onTodoEnter(e: Event; n: VNode) =
contents.insert(n.value & "BBBB")
contents.insert(n.value)
n.value = ""
proc createDom(): VNode =
result = buildHtml(tdiv):
input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
onkeyupenter = onTodoEnter, setFocus)
for c in contents:
tdiv:
text c
setRenderer createDom

27
tests/nativehtmlgen.nim Normal file
View file

@ -0,0 +1,27 @@
## Simple test that shows Karax can also do client-side HTML rendering.
import "../karax" / [karaxdsl, vdom]
when defined(js):
{.error: "Use 'nim c' to compile this example".}
template kxi(): int = 0
template addEventHandler(n: VNode; k: EventKind; action: string; kxi: int) =
n.setAttr($k, action)
let tab = buildHtml(table):
tr:
td:
text "Cell A"
td:
text "Cell B"
tr:
td:
text "Cell C"
td:
text "Cell D"
td:
a(href = "#/", onclick = "javascript:myFunc()"):
text"haha"
echo tab

1
tests/nim.cfg Normal file
View file

@ -0,0 +1 @@
--path: "../karax"

22
tests/tester.nim Normal file
View file

@ -0,0 +1,22 @@
# For now we only test that the things still compile.
import os
proc exec(cmd: string) =
if os.execShellCmd(cmd) != 0:
quit "command failed " & cmd
proc main =
exec("nim js tests/diffDomTests.nim")
exec("nim js tests/compiler_tests.nim")
exec("nim js examples/todoapp/todoapp.nim")
exec("nim js examples/scrollapp/scrollapp.nim")
exec("nim js examples/mediaplayer/playerapp.nim")
exec("nim js examples/carousel/carousel.nim")
exec("nim js -d:nodejs -r tests/difftest.nim")
exec("nim c tests/nativehtmlgen.nim")
for test in os.walkFiles("examples/*.nim"):
exec("nim js " & test)
main()

View file

@ -1,6 +0,0 @@
- Better virtual dom diff'ing algorithm: find longest common subsequence
- add CSS changes to todo example.nim
- handle onscroll events
- introduce a way to test karax applications
- introduce a way to do AJAX