diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 5e6c415..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,31 +0,0 @@ -# This is a basic workflow to help you get started with Actions - -name: CI - -# Controls when the action will run. -on: - # Triggers the workflow on push or pull request events but only for the master branch - push: - branches: [ master ] - pull_request: - branches: [ master ] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - # This workflow contains a single job called "build" - build: - # The type of runner that the job will run on - runs-on: ubuntu-latest - container: nimlang/nim - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 - # Runs a single command using the runners shell - - name: Tests - run: | - nimble develop -y - nim c -r tests/tester.nim diff --git a/.gitignore b/.gitignore index 842e140..42b07a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,2 @@ nimcache/ -/tests/tester -karax/tools/karun -*.code-workspace -*.exe -app.js -app.html +/tests/tester \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..0c60bd2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +sudo: false +language: c +os: linux +install: + - git clone https://github.com/nim-lang/nim + - cd nim + - git clone --depth 1 https://github.com/nim-lang/csources.git + - cd csources + - sh build.sh + - cd ../ + - bin/nim c koch + - ./koch boot -d:release + - cd .. +before_script: + - set -e + - export PATH=$(pwd)/nim/bin:$(pwd):$PATH +script: + - nim c -r tests/tester.nim diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index ac49796..0000000 --- a/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017 Xored Software, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/docs/docs.rst b/docs/docs.rst index 3eec1a4..8298ba9 100644 --- a/docs/docs.rst +++ b/docs/docs.rst @@ -10,17 +10,17 @@ and some extensions that allow for an efficient component system. These extensions are: ``VNodeKind.int`` - The node has a single integer field. + The node havs a single integer field. ``VNodeKind.bool`` The node has a single boolean field. -``VNodeKind.vthunk`` +``VNodeKint.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`` +``VNodeKint.dthunk`` The node is a `DOM thunk`:idx:. This means there is a function attached to it that produces the ``Node`` DOM structure on demand. @@ -53,6 +53,6 @@ Event system Karax does not abstract over the event system the DOM offers much: The same ``Event`` structure is used. Every callback has the -signature ``proc (ev: Event; n: VNode)`` or empty ``proc ()``. +signature ``proc (ev: Event; n: VNode)``. diff --git a/examples/button.nim b/examples/button.nim deleted file mode 100644 index d86fa82..0000000 --- a/examples/button.nim +++ /dev/null @@ -1,15 +0,0 @@ -include karax / prelude - -var lines: seq[kstring] = @[] - -proc createDom(): VNode = - result = buildHtml(tdiv): - button: - text "Say hello!" - proc onclick(ev: Event; n: VNode) = - lines.add "Hello simulated universe" - for x in lines: - tdiv: - text x - -setRenderer createDom diff --git a/examples/buttonlambda.nim b/examples/buttonlambda.nim deleted file mode 100644 index 2ddadff..0000000 --- a/examples/buttonlambda.nim +++ /dev/null @@ -1,14 +0,0 @@ -include karax / prelude -from future import `=>` - -var lines: seq[kstring] = @[] - -proc createDom(): VNode = - result = buildHtml(tdiv): - button(onclick = () => lines.add "Hello simulated universe"): - text "Say hello!" - for x in lines: - tdiv: - text x - -setRenderer createDom diff --git a/examples/carousel/carousel.html b/examples/carousel/carousel.html index 500e73a..979dd38 100644 --- a/examples/carousel/carousel.html +++ b/examples/carousel/carousel.html @@ -1,15 +1,15 @@ - - Carousel app - + + Carousel app + - + -
+
- + - + diff --git a/examples/carousel/carousel.nims b/examples/carousel/carousel.nims index cef2dec..1451fc0 100644 --- a/examples/carousel/carousel.nims +++ b/examples/carousel/carousel.nims @@ -1 +1 @@ ---path: "../../karax" +--path: "../../src" diff --git a/examples/config.nims b/examples/config.nims deleted file mode 100644 index 76ed4d9..0000000 --- a/examples/config.nims +++ /dev/null @@ -1 +0,0 @@ ---path: ".." diff --git a/examples/hellostyle.nim b/examples/hellostyle.nim deleted file mode 100644 index 716c2f0..0000000 --- a/examples/hellostyle.nim +++ /dev/null @@ -1,17 +0,0 @@ -include karax / prelude -import karax / vstyles - -proc createDom(): VNode = - result = buildHtml(tdiv): - tdiv(style = style(StyleAttr.color, "red".cstring)): - text "red" - tdiv(style = style(StyleAttr.color, "blue")): - text "blue" - # explicit `kstring` required for varargs overload - tdiv(style = style((fontStyle, "italic".kstring), (color, "orange".kstring))): - text "italic orange" - # can use a string directly - tdiv(style = "font-style: oblique; color: pink".toCss): - text "oblique pink" - -setRenderer createDom diff --git a/examples/hellouniverse.nim b/examples/hellouniverse.nim deleted file mode 100644 index 8343d07..0000000 --- a/examples/hellouniverse.nim +++ /dev/null @@ -1,12 +0,0 @@ -include karax / prelude -import random - -proc createDom(): VNode = - result = buildHtml(tdiv): - if rand(100) <= 50: - text "Hello World!" - else: - text "Hello Universe" - -randomize() -setRenderer createDom diff --git a/examples/helloworld.nim b/examples/helloworld.nim deleted file mode 100644 index 76c43db..0000000 --- a/examples/helloworld.nim +++ /dev/null @@ -1,7 +0,0 @@ -include karax / prelude - -proc createDom(): VNode = - result = buildHtml(tdiv): - text "Hello World!" - -setRenderer createDom diff --git a/examples/login.nim b/examples/login.nim deleted file mode 100644 index 0350e23..0000000 --- a/examples/login.nim +++ /dev/null @@ -1,46 +0,0 @@ -include karax / prelude -from sugar import `=>` -import karax / errors - -proc loginField(desc, field, class: kstring; - validator: proc (field: kstring): proc ()): VNode = - result = buildHtml(tdiv): - label(`for` = field): - text desc - input(class = class, id = field, onchange = validator(field)) - -# some consts in order to prevent typos: -const - username = kstring"username" - password = kstring"password" - -proc validateNotEmpty(field: kstring): proc () = - result = proc () = - let x = getVNodeById(field) - if x.text == "": - errors.setError(field, field & " must not be empty") - else: - errors.setError(field, "") - -var loggedIn: bool - -proc loginDialog(): VNode = - result = buildHtml(tdiv): - if not loggedIn: - loginField("Name: ", username, "input", validateNotEmpty) - loginField("Password: ", password, "password", validateNotEmpty) - button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()): - text "Login" - p: - text errors.getError(username) - p: - text errors.getError(password) - else: - p: - text "You are now logged in." - -setError username, username & " must not be empty" -setError password, password & " must not be empty" - -when not declared(toychat): - setRenderer loginDialog diff --git a/examples/mediaplayer/mediaplayer.nim b/examples/mediaplayer/mediaplayer.nim index c84b68b..7bc9c65 100644 --- a/examples/mediaplayer/mediaplayer.nim +++ b/examples/mediaplayer/mediaplayer.nim @@ -1,5 +1,5 @@ -import karax / [karax, karaxdsl, vdom, kdom, compact] +import karax, karaxdsl, vdom, kdom, compact const Play = 0 diff --git a/examples/mediaplayer/playerapp.html b/examples/mediaplayer/playerapp.html index a45a2e8..9e076e6 100644 --- a/examples/mediaplayer/playerapp.html +++ b/examples/mediaplayer/playerapp.html @@ -1,15 +1,15 @@ - - Player app - + + Player app + - + -
+
- + - + diff --git a/examples/mediaplayer/playerapp.nim b/examples/mediaplayer/playerapp.nim index acb2e68..e611eec 100644 --- a/examples/mediaplayer/playerapp.nim +++ b/examples/mediaplayer/playerapp.nim @@ -1,6 +1,6 @@ ## Example app that shows how to write and embed a custom component. -include karax/prelude +include karaxprelude import mediaplayer const url = "https://www.w3schools.com/html/mov_bbb.mp4" diff --git a/examples/mediaplayer/playerapp.nims b/examples/mediaplayer/playerapp.nims index cf2eb9e..9028ff1 100644 --- a/examples/mediaplayer/playerapp.nims +++ b/examples/mediaplayer/playerapp.nims @@ -1 +1 @@ ---path: "../.." +--path: "../../src" diff --git a/examples/scrollapp/scrollapp.html b/examples/scrollapp/scrollapp.html index 44d657e..1ff8b1b 100644 --- a/examples/scrollapp/scrollapp.html +++ b/examples/scrollapp/scrollapp.html @@ -1,15 +1,15 @@ - - Todo app - + + Todo app + - + -
+
- + - + diff --git a/examples/scrollapp/scrollapp.nim b/examples/scrollapp/scrollapp.nim index 4490447..8ead611 100644 --- a/examples/scrollapp/scrollapp.nim +++ b/examples/scrollapp/scrollapp.nim @@ -1,7 +1,7 @@ ## Example that shows how to accomplish an "infinitely scrolling" app. -include karax/prelude -import karax / [jstrutils, kdom, vstyles] +include karaxprelude +import jstrutils, kdom, vstyles var entries: seq[cstring] = @[] for i in 1..500: diff --git a/examples/scrollapp/scrollapp.nims b/examples/scrollapp/scrollapp.nims index cf2eb9e..9028ff1 100644 --- a/examples/scrollapp/scrollapp.nims +++ b/examples/scrollapp/scrollapp.nims @@ -1 +1 @@ ---path: "../.." +--path: "../../src" diff --git a/examples/todoapp/todoapp.html b/examples/todoapp/todoapp.html index ab3daa7..488a3bf 100644 --- a/examples/todoapp/todoapp.html +++ b/examples/todoapp/todoapp.html @@ -1,16 +1,16 @@ - - Todo app - - + + Todo app + + - + -
+
- + - + diff --git a/examples/todoapp/todoapp.nim b/examples/todoapp/todoapp.nim index e3720fd..47ddeae 100644 --- a/examples/todoapp/todoapp.nim +++ b/examples/todoapp/todoapp.nim @@ -1,5 +1,5 @@ -import karax / [vdom, karax, karaxdsl, jstrutils, compact, localstorage] +import vdom, karax, karaxdsl, jstrutils, compact, localstorage type Filter = enum @@ -9,7 +9,7 @@ var selectedEntry = -1 filter: Filter entriesLen: int - doneswitch = true + const contentSuffix = cstring"content" completedSuffix = cstring"completed" @@ -41,9 +41,8 @@ proc updateEntry(pos: int, content: cstring, completed: bool) = markAsCompleted(pos, completed) proc onTodoEnter(ev: Event; n: VNode) = - if n.value.strip() != "": - addEntry(n.value, false) - n.value = "" + addEntry(n.value, false) + n.value = "" proc removeHandler(ev: Event; n: VNode) = updateEntry(n.index, cstring(nil), false) @@ -62,9 +61,9 @@ proc toggleEntry(ev: Event; n: VNode) = markAsCompleted(id, not isCompleted(id)) proc onAllDone(ev: Event; n: VNode) = - for i in 0.. - + diff --git a/experiments/config.nims b/experiments/config.nims deleted file mode 100644 index 86673aa..0000000 --- a/experiments/config.nims +++ /dev/null @@ -1,2 +0,0 @@ ---path: "../karax" ---path: ".." diff --git a/experiments/echartstest.html b/experiments/echartstest.html deleted file mode 100644 index 8c9b225..0000000 --- a/experiments/echartstest.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - Karax ECharts Test - - - - -
- - - - diff --git a/experiments/echartstest.nim b/experiments/echartstest.nim deleted file mode 100644 index 950c6ce..0000000 --- a/experiments/echartstest.nim +++ /dev/null @@ -1,58 +0,0 @@ -import karax / [kbase, vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, jjson] - -type - Views = enum - Customers, Products - -var - currentView = Customers - -type - EChart* = ref object - -proc echartsInit(n: Element): EChart {.importc: "echarts.init".} -proc setOption(x: EChart; option: JsonNode) {.importcpp.} - -proc postRender(data: RouterData) = - if currentView == Products: - let myChart = echartsInit(kdom.getElementById("echartSection")) - # specify chart configuration item and data - let option = %*{ - "title": { - "text": "ECharts entry example" - }, - "tooltip": {}, - "legend": { - "data": ["Sales"] - }, - "xAxis": { - "data": ["shirt","cardign","chiffon shirt","pants","heels","socks"] - }, - "yAxis": {}, - "series": [{ - "name": "Sales", - "type": "bar", - "data": [5, 20, 36, 10, 10, 20] - }] - } - myChart.setOption(option) - -proc createDom(data: RouterData): VNode = - let hash = data.hashPart - if hash == cstring"#/Products": currentView = Products - else: currentView = Customers - - result = buildHtml(tdiv): - ul(class = "tabs"): - for v in low(Views)..high(Views): - li: - a(href = "#/" & $v): - text cstring($v) - - if currentView == Products: - tdiv(id = "echartSection", style = style((width, kstring"600px"), (height, kstring"400px"))) - tdiv: - text "other section" - -setRenderer createDom, "ROOT", postRender -setForeignNodeId "echartSection" diff --git a/experiments/menus.nim b/experiments/menus.nim deleted file mode 100644 index 6ea0529..0000000 --- a/experiments/menus.nim +++ /dev/null @@ -1,65 +0,0 @@ -## Example program that shows how to create menus with Karax. - -include prelude -import jstrutils, kdom - -proc contentA(): VNode = - result = buildHtml(tdiv): - text "content A" - -proc contentB(): VNode = - result = buildHtml(tdiv): - text "content B" - -proc contentC(): VNode = - result = buildHtml(tdiv): - text "content C" - - - -type - MenuItemHandler = proc(): VNode - -var content: MenuItemHandler = contentA - -proc menuAction(x: MenuItemHandler): proc() = - result = proc() = content = x - -proc buildMenu(): VNode = - result = buildHtml(tdiv): - nav(class="navbar is-primary"): - tdiv(class="navbar-brand"): - a(class="navbar-item", onclick = menuAction(contentA)): - strong: - text "My awesome menu" - - tdiv(id="navMenuTransparentExample", class="navbar-menu"): - tdiv(class = "navbar-start"): - tdiv(class="navbar-item has-dropdown is-hoverable"): - a(class="navbar-link", onclick = menuAction(contentB)): - text "Masters" - tdiv(class="navbar-dropdown is-boxed"): - a(class="navbar-item", onclick = menuAction(contentC)): - text "Inventory" - a(class="navbar-item", onclick = menuAction(contentC)): - text "Product" - hr(class="navbar-divider"): - a(class="navbar-item", onclick = menuAction(contentC)): - text "Product" - - tdiv(class="navbar-item has-dropdown is-hoverable"): - a(class="navbar-link", onclick = menuAction(contentC)): - text "Transactions" - tdiv(class = "navbar-dropdown is-boxed", id="blogDropdown"): - a(class="navbar-item", onclick = menuAction(contentC)): - text "Purchase Bill" - a(class="navbar-item", onclick = menuAction(contentC)): - text "Purchase Return" - a(class="navbar-item", onclick = menuAction(contentC)): - text "Sale Bill" - hr(class="navbar-divider"): - a(class="navbar-item", onclick = menuAction(contentC)): - text "Stock Adjustment" - content() - -setRenderer buildMenu diff --git a/experiments/nativenodes.nim b/experiments/nativenodes.nim deleted file mode 100644 index 9d255ee..0000000 --- a/experiments/nativenodes.nim +++ /dev/null @@ -1,18 +0,0 @@ - - -include karax / prelude -import karax / kdom - -proc myInput: VNode = - result = buildHtml: - input() - -var inp = vnodeToDom(myInput()) - -proc myAwesomeComponent(x: Node): VNode = - result = dthunk(x) - -proc main: VNode = - result = myAwesomeComponent(inp) - -setRenderer main diff --git a/experiments/refs/demo.html b/experiments/refs/demo.html index 941333a..e1fe419 100644 --- a/experiments/refs/demo.html +++ b/experiments/refs/demo.html @@ -8,6 +8,6 @@
- + diff --git a/experiments/scrollapp.html b/experiments/scrollapp.html index 8b909f1..ad3ce2f 100644 --- a/experiments/scrollapp.html +++ b/experiments/scrollapp.html @@ -10,7 +10,7 @@
- + diff --git a/experiments/scrollapp_simple.nim b/experiments/scrollapp_simple.nim deleted file mode 100644 index cdd19cd..0000000 --- a/experiments/scrollapp_simple.nim +++ /dev/null @@ -1,25 +0,0 @@ -## Example that shows how to accomplish an "infinitely scrolling" app. - -include karax / prelude -import karax / [kdom, vstyles] - -var entries: seq[cstring] = @[] -for i in 1..500: - entries.add(cstring("Entry ") & &i) - -proc scrollEvent(ev: Event; n: VNode) = - let d = n.dom - if d != nil and inViewport(d.lastChild): - # "load" more data: - for i in 1..50: - entries.add(cstring("Loaded Entry ") & &i) - -proc createDom(): VNode = - result = buildHtml(): - tdiv(onscroll=scrollEvent, style=style( - (StyleAttr.height, cstring"400px"), (StyleAttr.overflow, cstring"scroll"))): - for x in entries: - tdiv: - text x - -setRenderer createDom diff --git a/experiments/trello/knete.nim b/experiments/trello/knete.nim deleted file mode 100644 index 3a09ee3..0000000 --- a/experiments/trello/knete.nim +++ /dev/null @@ -1,752 +0,0 @@ -## Raw DOM manipulation. No DOM diff'ing, no cry. - -import macros, tables -from strutils import startsWith, toLowerAscii, toUpperAscii - -when defined(js): - type kstring* = cstring -else: - type kstring* = string - -type - Tag* {.pure.} = enum - text = "#text", - - html, head, title, base, link, meta, style, - script, noscript, - body, section, nav, article, aside, - h1, h2, h3, h4, h5, h6, - header, footer, address, main - - p, hr, pre, blockquote, ol, ul, li, - dl, dt, dd, - figure, figcaption, - - tdiv = "div", - - a, em, strong, small, - strikethrough = "s", cite, quote, - dfn, abbr, data, time, code, `var` = "var", samp, - kdb, sub, sup, italic = "i", bold = "b", underlined = "u", - mark, ruby, rt, rp, bdi, dbo, span, br, wbr, - ins, del, img, iframe, embed, `object` = "object", - param, video, audio, source, track, canvas, map, - area, svg, math, - - table, caption, colgroup, col, tbody, thead, - tfoot, tr, td, th, - - form, fieldset, legend, label, input, button, - select, datalist, optgroup, option, textarea, - keygen, output, progress, meter, - details, summary, command, menu - -type - EventKind* {.pure.} = enum ## The events supported by the virtual DOM. - onclick, ## An element is clicked. - oncontextmenu, ## An element is right-clicked. - ondblclick, ## An element is double clicked. - onkeyup, ## A key was released. - onkeydown, ## A key is pressed. - onkeypressed, # A key was pressed. - onfocus, ## An element got the focus. - onblur, ## An element lost the focus. - onchange, ## The selected value of an element was changed. - onscroll, ## The user scrolled within an element. - - onmousedown, ## A pointing device button (usually a mouse) is pressed - ## on an element. - onmouseenter, ## A pointing device is moved onto the element that - ## has the listener attached. - onmouseleave, ## A pointing device is moved off the element that - ## has the listener attached. - onmousemove, ## A pointing device is moved over an element. - onmouseout, ## A pointing device is moved off the element that - ## has the listener attached or off one of its children. - onmouseover, ## A pointing device is moved onto the element that has - ## the listener attached or onto one of its children. - onmouseup, ## A pointing device button is released over an element. - - ondrag, ## An element or text selection is being dragged (every 350ms). - ondragend, ## A drag operation is being ended (by releasing a mouse button - ## or hitting the escape key). - ondragenter, ## A dragged element or text selection enters a valid drop target. - ondragleave, ## A dragged element or text selection leaves a valid drop target. - ondragover, ## An element or text selection is being dragged over a valid - ## drop target (every 350ms). - ondragstart, ## The user starts dragging an element or text selection. - ondrop, ## An element is dropped on a valid drop target. - - onsubmit, ## A form is submitted - oninput, ## An input value changes - - onkeyupenter, ## vdom extension: an input field received the ENTER key press - onkeyuplater ## vdom extension: a key was pressed and some time - ## passed (useful for on-the-fly text completions) - - -macro buildLookupTables(): untyped = - var a = newTree(nnkBracket) - for i in low(Tag)..high(Tag): - let x = $i - let y = if x[0] == '#': x else: toUpperAscii(x) - a.add(newCall("kstring", newLit(y))) - var e = newTree(nnkBracket) - for i in low(EventKind)..high(EventKind): - e.add(newCall("kstring", newLit(substr($i, 2)))) - - template tmpl(a, e) {.dirty.} = - const - toTagName*: array[Tag, kstring] = a - toEventName*: array[EventKind, kstring] = e - - result = getAst tmpl(a, e) - -buildLookupTables() - -# ---------------- CSS ---------------------------------------------- - -type - StyleAttr* {.pure.} = enum - ## The style attributes supported by the virtual DOM. - ## Reference: https://www.w3schools.com/jsref/dom_obj_style.asp - alignContent - alignItems - alignSelf - animation - animationDelay - animationDirection - animationDuration - animationFillMode - animationIterationCount - animationName - animationTimingFunction - animationPlayState - background - backgroundAttachment - backgroundColor - backgroundImage - backgroundPosition - backgroundRepeat - backgroundClip - backgroundOrigin - backgroundSize - backfaceVisibility - border - borderBottom - borderBottomColor - borderBottomLeftRadius - borderBottomRightRadius - borderBottomStyle - borderBottomWidth - borderCollapse - borderColor - borderImage - borderImageOutset - borderImageRepeat - borderImageSlice - borderImageSource - borderImageWidth - borderLeft - borderLeftColor - borderLeftStyle - borderLeftWidth - borderRadius - borderRight - borderRightColor - borderRightStyle - borderRightWidth - borderSpacing - borderStyle - borderTop - borderTopColor - borderTopLeftRadius - borderTopRightRadius - borderTopStyle - borderTopWidth - borderWidth - bottom - boxDecorationBreak - boxShadow - boxSizing - captionSide - clear - clip - color - columnCount - columnFill - columnGap - columnRule - columnRuleColor - columnRuleStyle - columnRuleWidth - columns - columnSpan - columnWidth - content - counterIncrement - counterReset - cursor - direction - display - emptyCells - filter - flex - flexBasis - flexDirection - flexFlow - flexGrow - flexShrink - flexWrap - cssFloat - font - fontFamily - fontSize - fontSizeAdjust - fontStretch - fontStyle - fontVariant - fontWeight - hangingPunctuation - height - hyphens - icon - imageOrientation - justifyContent - left - letterSpacing - lineHeight - listStyle - listStyleImage - listStylePosition - listStyleType - margin - marginBottom - marginLeft - marginRight - marginTop - maxHeight - maxWidth - minHeight - minWidth - navDown - navIndex - navLeft - navRight - navUp - opacity - order - orphans - outline - outlineColor - outlineOffset - outlineStyle - outlineWidth - overflow - overflowX - overflowY - padding - paddingBottom - paddingLeft - paddingRight - paddingTop - pageBreakAfter - pageBreakBefore - pageBreakInside - perspective - perspectiveOrigin - pointerEvents - position - quotes - resize - right - scrollbar3dLightColor # note: missing in w3schools reference - scrollbarArrowColor # note: missing in w3schools reference - scrollbarBaseColor # note: missing in w3schools reference - scrollbarDarkshadowColor # note: missing in w3schools reference - scrollbarFaceColor # note: missing in w3schools reference - scrollbarHighlightColor # note: missing in w3schools reference - scrollbarShadowColor # note: missing in w3schools reference - scrollbarTrackColor # note: missing in w3schools reference - tableLayout - tabSize - textAlign - textAlignLast - textDecoration - textDecorationColor - textDecorationLine - textDecorationStyle - textIndent - textJustify - textOverflow - textShadow - textTransform - top - transform - transformOrigin - transformStyle - transition - transitionDelay - transitionDuration - transitionProperty - transitionTimingFunction - unicodeBidi - userSelect - verticalAlign - visibility - whiteSpace - width - wordBreak - wordSpacing - wordWrap - widows - zIndex - -macro buildStyleLookupTable(): untyped = - var e = newTree(nnkBracket) - for i in low(StyleAttr)..high(StyleAttr): - e.add(newCall("kstring", newLit($i))) - template tmpl(e) {.dirty.} = - const - toStyleAttrName: array[StyleAttr, kstring] = e - result = getAst tmpl(e) - -buildStyleLookupTable() - -type - StyleObj {.importc.} = object - background*: cstring - backgroundAttachment*: cstring - backgroundColor*: cstring - backgroundImage*: cstring - backgroundPosition*: cstring - backgroundRepeat*: cstring - border*: cstring - borderBottom*: cstring - borderBottomColor*: cstring - borderBottomStyle*: cstring - borderBottomWidth*: cstring - borderColor*: cstring - borderLeft*: cstring - borderLeftColor*: cstring - borderLeftStyle*: cstring - borderLeftWidth*: cstring - borderRight*: cstring - borderRightColor*: cstring - borderRightStyle*: cstring - borderRightWidth*: cstring - borderStyle*: cstring - borderTop*: cstring - borderTopColor*: cstring - borderTopStyle*: cstring - borderTopWidth*: cstring - borderWidth*: cstring - bottom*: cstring - captionSide*: cstring - clear*: cstring - clip*: cstring - color*: cstring - cursor*: cstring - direction*: cstring - display*: cstring - emptyCells*: cstring - cssFloat*: cstring - font*: cstring - fontFamily*: cstring - fontSize*: cstring - fontStretch*: cstring - fontStyle*: cstring - fontVariant*: cstring - fontWeight*: cstring - height*: cstring - left*: cstring - letterSpacing*: cstring - lineHeight*: cstring - listStyle*: cstring - listStyleImage*: cstring - listStylePosition*: cstring - listStyleType*: cstring - margin*: cstring - marginBottom*: cstring - marginLeft*: cstring - marginRight*: cstring - marginTop*: cstring - maxHeight*: cstring - maxWidth*: cstring - minHeight*: cstring - minWidth*: cstring - overflow*: cstring - padding*: cstring - paddingBottom*: cstring - paddingLeft*: cstring - paddingRight*: cstring - paddingTop*: cstring - pageBreakAfter*: cstring - pageBreakBefore*: cstring - pointerEvents*: cstring - position*: cstring - right*: cstring - scrollbar3dLightColor*: cstring - scrollbarArrowColor*: cstring - scrollbarBaseColor*: cstring - scrollbarDarkshadowColor*: cstring - scrollbarFaceColor*: cstring - scrollbarHighlightColor*: cstring - scrollbarShadowColor*: cstring - scrollbarTrackColor*: cstring - tableLayout*: cstring - textAlign*: cstring - textDecoration*: cstring - textIndent*: cstring - textTransform*: cstring - transform*: cstring - top*: cstring - verticalAlign*: cstring - visibility*: cstring - width*: cstring - wordSpacing*: cstring - zIndex*: int - Style* = ref StyleObj - - ElementObj {.importc.} = object - id*, class*, value*: cstring - disabled*: bool - style*: Style - Element* = ref ElementObj - - DocumentObj {.importc.} = object - Document = ref DocumentObj - -proc setStyle(s: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.} - -proc applyStyles*(e: Element; pairs: openArray[(StyleAttr, kstring)]) = - for x in pairs: - e.style.setStyle(toStyleAttrName[x[0]], x[1]) - -# ------- Tree manipulation --------------------------- - -var document {.importc.}: Document - -proc parent*(x: Element): Element {.importcpp: "#.parentNode".} - -proc up*(x: Element; className: cstring): Element = - result = x - while result != nil and result.class != className: - result = result.parent - -proc len*(x: Element): int {.importcpp: "#.childNodes.length".} -proc `[]`*(x: Element; idx: int): Element {.importcpp: "#.childNodes[#]".} -proc getElementById*(id: cstring): Element {.importc: "document.getElementById", nodecl.} -proc add*(n, child: Element) {.importcpp: "appendChild".} - -proc removeChild(n, child: Element) {.importcpp.} -proc replaceChild(n, newNode, oldNode: Element) {.importcpp.} - -proc replace*(self, by: Element) = replaceChild(self.parent, by, self) -proc delete*(self: Element) = removeChild(self.parent, self) - -proc insertBefore(n, newNode, before: Element) {.importcpp.} -proc insert*(before, newNode: Element) = insertBefore(before.parent, newNode, before) - -proc createTextNode(d: Document, text: cstring): Element {.importcpp.} -proc text*(s: kstring): Element = createTextNode(document, s) - -iterator items*(n: Element): Element = - for i in 0.. 0: - result[L-1] = tcall2(result[L-1], tmpContext) - of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkTryStmt, - nnkFinally: - # recurse for every child: - result = copyNimNode(n) - for x in n: - result.add tcall2(x, tmpContext) - of nnkCaseStmt: - # recurse for children, but don't add call for case ident - result = copyNimNode(n) - result.add n[0] - for i in 1 ..< n.len: - result.add tcall2(n[i], tmpContext) - of nnkProcDef: - let name = getName n[0] - if name.startsWith"on": - # turn it into an anon proc: - let anon = copyNimTree(n) - anon[0] = newEmptyNode() - if tmpContext == nil: - error "no Element to attach the event handler to" - else: - result = newCall(evHandler(), tmpContext, - newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi")) - else: - result = n - of nnkVarSection, nnkLetSection, nnkConstSection: - result = n - of nnkCallKinds - {nnkInfix}: - let op = getName(n[0]) - if isComponent(op) == ComponentKind.Tag: - let tmp = genSym(nskLet, "tmp") - let call = newCall(bindSym"tree", newDotExpr(bindSym"Tag", n[0])) - result = newTree( - if tmpContext == nil: nnkStmtListExpr else: nnkStmtList, - newLetStmt(tmp, call)) - for i in 1 ..< n.len: - # named parameters are transformed into attributes or events: - let x = n[i] - if x.kind == nnkExprEqExpr: - let key = getName x[0] - if key.startsWith("on"): - result.add newCall(evHandler(), - tmp, newDotExpr(bindSym"EventKind", x[0]), x[1]) - elif key in SpecialAttrs: - result.add newDotAsgn(tmp, key, x[1]) - elif eqIdent(key, "style"): - result.add newCall(bindSym"applyStyles", tmp, toKstring x[1]) - elif eqIdent(key, "setFocus"): - result.add newCall(bindSym"focus", tmp) - else: - result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1]) - elif eqIdent(x, "setFocus"): - result.add newCall(bindSym"focus", tmp) - else: - result.add tcall2(x, tmp) - if tmpContext == nil: - result.add tmp - else: - result.add newCall(bindSym"add", tmpContext, tmp) - elif tmpContext != nil and op notin StmtContext: - var hasEventHandlers = false - for i in 1..= 0.18.0" -requires "ws" -requires "dotenv" -skipDirs = @["examples", "experiments", "tests"] +requires "nim >= 0.16.1" -bin = @["karax/tools/karun"] -installExt = @["nim"] +skipDirs = @["examples/todoapp"] \ No newline at end of file diff --git a/karax/autocomplete.nim b/karax/autocomplete.nim deleted file mode 100644 index 210ed4a..0000000 --- a/karax/autocomplete.nim +++ /dev/null @@ -1,160 +0,0 @@ - -import karax, karaxdsl, vdom, kdom, jstrutils - -type - AutocompleteState* = ref object - choices, candidates: seq[cstring] - selected, maxMatches: int - showCandidates, controlPressed: bool - -proc newAutocomplete*(choices: seq[cstring]; maxMatches = 5): AutocompleteState = - ## Creates a new state for the autocomplete widget. ``maxMatches`` is the maximum - ## number of elements to show. - AutocompleteState(choices: choices, candidates: @[], - selected: -1, maxMatches: maxMatches, showCandidates: false, - controlPressed: false) - -proc autocomplete*(s: AutocompleteState; onselection: proc(s: cstring)): VNode = - var inp: VNode - - proc commit(ev: Event) = - s.showCandidates = false - onselection(inp.dom.value) - when false: - if inp.dom != nil: - echo "setting to A ", inp.dom.value.isNil - result.text = inp.dom.value - else: - echo "setting to B ", inp.text.isNil - result.text = inp.text - for e in result.events: - if e[0] == EventKind.onchange: - e[1](ev, result) - - proc onkeyuplater(ev: Event; n: VNode) = - if not s.controlPressed: - let v = n.value - s.candidates.setLen 0 - for c in s.choices: - if v.len == 0 or c.containsIgnoreCase(v): s.candidates.add(c) - - proc onkeydown(ev: Event; n: VNode) = - const - LEFT = 37 - UP = 38 - RIGHT = 39 - DOWN = 40 - TAB = 9 - ESC = 27 - ENTER = 13 - # UP: Move focus to the previous item. If on first item, move focus to the input. - # If on the input, move focus to last item. - # DOWN: Move focus to the next item. If on last item, move focus to the input. - # If on the input, move focus to the first item. - # ESCAPE: Close the menu. - # ENTER: Select the currently focused item and close the menu. - # TAB: Select the currently focused item, close the menu, and - # move focus to the next focusable element - s.controlPressed = false - case ev.keyCode - of UP: - s.controlPressed = true - s.showCandidates = true - if s.selected > 0: - dec s.selected - n.setInputText s.candidates[s.selected] - of DOWN: - s.controlPressed = true - s.showCandidates = true - if s.selected < s.candidates.len - 1: - inc s.selected - n.setInputText s.candidates[s.selected] - of ESC: - s.showCandidates = false - s.controlPressed = true - of ENTER: - s.controlPressed = true -# inp.setInputText s.choices[i] - commit(ev) - else: - discard - - proc window(s: AutocompleteState): (int, int) = - var first, last: int - if s.selected >= 0: - first = s.selected - (s.maxMatches div 2) - last = s.selected + (s.maxMatches div 2) - if first < 0: first = 0 - # too few because we had to trim first? - if (last - first + 1) < s.maxMatches: last = first + s.maxMatches - 1 - else: - first = 0 - last = s.maxMatches - 1 - if last > high(s.candidates): last = high(s.candidates) - # still too few because we're at the end? - if (last - first + 1) < s.maxMatches: - first = last - s.maxMatches + 1 - if first < 0: first = 0 - - result = (first, last) - - inp = buildHtml: - input(onkeyuplater = onkeyuplater, - onkeydown = onkeydown, - # onblur = proc (ev: Event; n: VNode) = commit(ev), - onfocus = proc (ev: Event; n: VNode) = - onkeyuplater(ev, n) - s.showCandidates = true) - - proc select(i: int): proc(ev: Event; n: VNode) = - result = proc(ev: Event; n: VNode) = - s.selected = i - s.showCandidates = false - inp.setInputText s.choices[i] - commit(ev) - - result = buildHtml(table): - tr: - td: - inp - if s.showCandidates: - let (first, last) = window(s) - for i in first..last: - tr: - td(onclick = select(i), class = cstring"button " & - (if i == s.selected: cstring"is-primary" else: "")): - text s.candidates[i] - -when isMainModule: - const suggestions = @[cstring"ActionScript", - "AppleScript", - "Asp", - "BASIC", - "C", - "C++", - "Clojure", - "COBOL", - "Erlang", - "Fortran", - "Groovy", - "Haskell", - "Java", - "JavaScript", - "Lisp", - "Nim", - "Perl", - "PHP", - "Python", - "Ruby", - "Scala", - "Scheme"] - - var s = newAutocomplete(suggestions) - - proc main(): VNode = - result = buildHtml(tdiv): - autocomplete(s, proc (s: cstring) = echo "now ", s)#: - # proc onchange(ev: Event; n: VNode) = - # echo "now selected ", n.kind - - setRenderer(main) diff --git a/karax/compact.nim b/karax/compact.nim deleted file mode 100644 index 4e5dd6d..0000000 --- a/karax/compact.nim +++ /dev/null @@ -1,114 +0,0 @@ -## Components in Karax are built by the ``.component`` macro annotation. - -when defined(js): - import jdict, kdom - -import macros, vdom, tables, strutils, kbase - -when defined(js): - var - vcomponents* = newJDict[cstring, proc(args: seq[VNode]): VNode]() -else: - var - vcomponents* = newTable[kstring, proc(args: seq[VNode]): VNode]() - -type - ComponentKind* {.pure.} = enum - None, - Tag, - VNode - -var - allcomponents {.compileTime.} = initTable[string, ComponentKind]() - -proc isComponent*(x: string): ComponentKind {.compileTime.} = - allcomponents.getOrDefault(x) - -proc addTags() {.compileTime.} = - let x = (bindSym"VNodeKind").getTypeImpl - expectKind(x, nnkEnumTy) - for i in ord(VNodeKind.html)..ord(VNodeKind.high): - # +1 because of empty node at the start of the enum AST: - let tag = $x[i+1] - allcomponents[tag] = ComponentKind.Tag - -static: - addTags() - -proc unpack(symbolicType: NimNode; index: int): NimNode {.compileTime.} = - #let t = symbolicType.getTypeImpl - let t = repr(symbolicType) - case t - of "cstring": - result = quote do: - args[`index`].text - of "int", "VKey": - result = quote do: - args[`index`].intValue - of "bool": - result = quote do: - args[`index`].intValue != 0 - elif t.endsWith"Kind": - result = quote do: - `symbolicType`(args[`index`].intValue) - else: - # just pass it along, maybe there is some conversion for it: - result = quote do: - args[`index`] - -proc newname*(n: NimNode): NimNode = - if n.kind == nnkPostfix: - n[1] = newname(n[1]) - result = n - elif n.kind == nnkSym: - result = ident(n.strVal) - else: - result = n - -when defined(js): - macro compact*(prc: untyped): untyped = - ## A 'compact' tree generation proc is one that only depends on its - ## inputs and should be stored as a compact virtual DOM tree and - ## only expanded on demand (when its inputs changed). - var n = prc.copyNimNode - for i in 0..6: n.add prc[i].copyNimTree - expectKind(n, nnkProcDef) - if n[0].kind == nnkEmpty: - error("please pass a non anonymous proc", n[0]) - let name = n[0] - let params = params(n) - let rettype = repr params[0] - var isvirtual = ComponentKind.None - if rettype == "VNode": - isvirtual = ComponentKind.VNode - else: - error "component must return VNode", params[0] - let realName = if name.kind == nnkPostfix: name[1] else: name - let nn = $realName - n[0] = ident("inner" & nn) - var unpackCall = newCall(n[0]) - var counter = 0 - for i in 1 ..< params.len: - let param = params[i] - let L = param.len - let typ = param[L-2] - for j in 0 .. L-3: - unpackCall.add unpack(typ, counter) - inc counter - - template vwrapper(pname, unpackCall) {.dirty.} = - proc pname(args: seq[VNode]): VNode = - unpackCall - - template vregister(key, val) = - bind jdict.`[]=` - `[]=`(vcomponents, kstring(key), val) - - result = newTree(nnkStmtList, n) - - if isvirtual == ComponentKind.VNode: - result.add getAst(vwrapper(newname name, unpackCall)) - result.add getAst(vregister(newLit(nn), realName)) - allcomponents[nn] = isvirtual - when defined(debugKaraxDsl): - echo repr result diff --git a/karax/errors.nim b/karax/errors.nim deleted file mode 100644 index aedd103..0000000 --- a/karax/errors.nim +++ /dev/null @@ -1,32 +0,0 @@ -## Error handling logic for form input validation. - -import karax, jdict - -var - gerrorMsgs = newJDict[cstring, cstring]() - gerrorCounter = 0 - -proc hasErrors*(): bool = gerrorCounter != 0 - -proc hasError*(field: cstring): bool = gerrorMsgs.contains(field) and len(gerrorMsgs[field]) > 0 - -proc disableOnError*(): cstring = toDisabled(hasErrors()) - -proc getError*(field: cstring): cstring = - if not gerrorMsgs.contains(field): - result = "" - else: - result = gerrorMsgs[field] - -proc setError*(field, msg: cstring) = - let previous = getError(field) - if len(msg) == 0: - if len(previous) != 0: dec(gerrorCounter) - else: - if len(previous) == 0: inc(gerrorCounter) - gerrorMsgs[field] = msg - -proc clearErrors*() = - let m = gerrorMsgs - for k in m.keys: - setError(k, "") diff --git a/karax/jjson.nim b/karax/jjson.nim deleted file mode 100644 index ad6106b..0000000 --- a/karax/jjson.nim +++ /dev/null @@ -1,67 +0,0 @@ -## 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.. - -""" - -const html = """ - - - - - $1 - $2 - - -
- -$3 - - -""" -const websocket = """ - -""" - -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() diff --git a/karax/tools/karun.nims b/karax/tools/karun.nims deleted file mode 100644 index 8983496..0000000 --- a/karax/tools/karun.nims +++ /dev/null @@ -1 +0,0 @@ -switch("threads", "on") \ No newline at end of file diff --git a/karax/tools/static_server.nim b/karax/tools/static_server.nim deleted file mode 100644 index eeddf4f..0000000 --- a/karax/tools/static_server.nim +++ /dev/null @@ -1,141 +0,0 @@ -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() diff --git a/readme.md b/readme.md deleted file mode 100644 index d8a3814..0000000 --- a/readme.md +++ /dev/null @@ -1,394 +0,0 @@ -![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 ``
`` 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). diff --git a/readme.rst b/readme.rst new file mode 100644 index 0000000..3fac7ad --- /dev/null +++ b/readme.rst @@ -0,0 +1,38 @@ +Karax – Single page applications in Nim |travis| +================================================ + +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:: + + nimble install # For first time + + 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. But don't take my +word for it, look at this: + +.. image:: docs/benchmark.png + + +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. + +.. |travis| image:: https://travis-ci.org/pragmagic/karax.svg?branch=master + :target: https://travis-ci.org/pragmagic/karax diff --git a/src/compact.nim b/src/compact.nim new file mode 100644 index 0000000..51de228 --- /dev/null +++ b/src/compact.nim @@ -0,0 +1,121 @@ +## Components in Karax are built by the ``.component`` macro annotation. + +import macros, jdict, kdom, vdom, tables, strutils + +var + vcomponents* = newJDict[cstring, proc(args: seq[VNode]): VNode]() + dcomponents* = newJDict[cstring, proc(args: seq[VNode]): Node]() + +type + ComponentKind* {.pure.} = enum + None, + Tag, + VNode, + Node + +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.symbol) + else: + result = n + +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 + elif rettype == "Node": + isvirtual = ComponentKind.Node + else: + error "component must return VNode or Node", 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.. =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.} diff --git a/src/kajax.nim b/src/kajax.nim new file mode 100644 index 0000000..c226c50 --- /dev/null +++ b/src/kajax.nim @@ -0,0 +1,53 @@ +## Karax -- Single page applications for Nim. + +## This module implements support for `ajax`:idx: socket +## handling. + +import karax + +proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)]; + data: cstring; + cont: proc (httpStatus: int; response: cstring); + kxi: KaraxInstance = kxi) = + proc contWrapper(httpStatus: int; response: cstring) = + cont(httpStatus, response) + redraw(kxi) + + type + HttpRequest {.importc.} = ref object + ThisObj {.importc.} = ref object + readyState, status: int + responseText, statusText: cstring + + 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 open(r: HttpRequest; meth, url: cstring; async: bool) {.importcpp: "#.open(@)".} + proc newRequest(): HttpRequest {.importcpp: "new XMLHttpRequest(@)".} + + 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.statusText) + ajax.send(data) + +proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)]; + data: cstring; + cont: proc (httpStatus: int, response: cstring); + kxi: KaraxInstance = kxi) = + ajax("POST", url, headers, data, cont, kxi) + +proc ajaxGet*(url: cstring; headers: openarray[(cstring, cstring)]; + cont: proc (httpStatus: int, response: cstring); + kxi: KaraxInstance = kxi) = + ajax("GET", url, headers, nil, cont, kxi) + +proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".} +proc fromJson*[T](blob: cstring): T {.importc: "JSON.parse".} diff --git a/karax/karax.nim b/src/karax.nim similarity index 59% rename from karax/karax.nim rename to src/karax.nim index 2246676..aef2730 100644 --- a/karax/karax.nim +++ b/src/karax.nim @@ -2,21 +2,18 @@ import kdom, vdom, jstrutils, compact, jdict, vstyles -export kdom.Event, kdom.Blob - -when defined(nimNoNil): - {.experimental: "notnil".} +export kdom.Event proc kout*[T](x: T) {.importc: "console.log", varargs.} - ## The preferred way of debugging karax applications. + ## the preferred way of debugging karax applications. type PatchKind = enum - pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach, pkSame + pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach Patch = object k: PatchKind parent, current: Node - newNode, oldNode: VNode + n: VNode PatchV = object parent, newChild: VNode pos: int @@ -25,19 +22,15 @@ type 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.} + rootId: cstring not nil + renderer: proc (): VNode {.closure.} currentTree: VNode - postRenderCallback: proc (data: RouterData) + postRenderCallback: proc () toFocus: Node toFocusV: VNode renderId: int - rendering: bool patches: seq[Patch] # we reuse this to save allocations patchLen: int patchesV: seq[PatchV] @@ -45,10 +38,8 @@ type runCount: int components: seq[ComponentPair] surpressRedraws*: bool - byId: JDict[cstring, VNode] when defined(stats): recursion: int - orphans: JDict[cstring, bool] var @@ -78,24 +69,6 @@ template keyeventBody() = # 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..>" & depth) - elif n.kind == VNodeKind.text: - kout depth, n.text - for i in 0 ..< n.len: - printV(n[i], depth & " ") +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) = + na: 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)) + kxi.patches.add(Patch(k: ka, parent: parenta, current: currenta, n: na)) 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 + kxi.patches[L].n = na inc kxi.patchLen proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) = @@ -382,41 +311,26 @@ proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) = kxi.patchesV[L].pos = pos inc kxi.patchLenV -proc moveDom(dest, src: VNode) = - dest.dom = src.dom - src.dom = nil - if dest.id != nil: - kxi.byId[dest.id] = dest - assert dest.len == src.len - for i in 0.. We cannot ever use the old VDOM, we have to use the new virtual -# DOM. For identical nodes we need to take over the .dom field from the old -# node since we don't recompute them. This must be done recursively. In -# vnodeToDom we have to check whether the 'dom' field was already set. If so, -# There is nothing to do. -# "Similar" nodes can have the opposite effect; consider -# -# AAAABAAAA -# AAAACDAAAA -# -# In this example B did change to C and 'D' is new. However, replacing B by -# CD is fine. -# - - -proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance) = +proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): EqResult = when defined(stats): if kxi.recursion > 100: - echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode, false) + echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode) if oldNode.kind == VNodeKind.text: echo oldNode.text + #return + #doAssert false, "overflow!" inc kxi.recursion - let result = eq(newNode, oldNode, false) + result = eq(newNode, oldNode) case result of componentsIdentical: kxi.components.add ComponentPair(oldNode: VComponent(oldNode), @@ -478,54 +370,90 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance) = if newNode.events.len != 0 or oldNode.events.len != 0: mergeEvents(newNode, oldNode, kxi) + when false: + if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea: + if oldNode.text != newNode.text: + oldNode.text = newNode.text + oldNode.dom.value = newNode.text let newLength = newNode.len let oldLength = oldNode.len - if newLength == 0 and oldLength == 0: return + if newLength == 0 and oldLength == 0: return result let minLength = min(newLength, oldLength) assert oldNode.kind == newNode.kind var commonPrefix = 0 + let isSpecial = oldNode.kind == VNodeKind.component or + oldNode.kind == VNodeKind.vthunk or + oldNode.kind == VNodeKind.dthunk + template eqAndUpdate(a: VNode; i: int; b: VNode; j: int; info, action: untyped) = + let oldLen = kxi.patchLen + let oldLenV = kxi.patchLenV + assert i < a.len + assert j < b.len + let r = if isSpecial: + diff(a[i], b[j], parent, current, kxi) + else: + diff(a[i], b[j], current, current.childNodes[j], kxi) + case r + of identical, componentsIdentical, similar: + a[i] = b[j] + action + of usenewNode: + kxi.addPatchV(b, j, a[i]) + action + # unfortunately, we need to propagate the changes upwards: + result = useNewNode + of different: + # undo what 'diff' would have done: + kxi.patchLen = oldLen + kxi.patchLenV = oldLenV + if result != different: result = r + break # compute common prefix: while commonPrefix < minLength: - if eq(newNode[commonPrefix], oldNode[commonPrefix], true) == identical: - kxi.addPatch(pkSame, nil, nil, newNode[commonPrefix], oldNode[commonPrefix]) + eqAndUpdate(newNode, commonPrefix, oldNode, commonPrefix, cstring"prefix"): 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]) + eqAndUpdate(newNode, newPos, oldNode, oldPos, cstring"suffix"): 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) + let r = diff(newNode[i], oldNode[i], current, current.childNodes[i], kxi) + if r == usenewNode: + #oldNode[i] = newNode[i] + kxi.addPatchV(oldNode, i, newNode[i]) + elif r != different: + newNode[i] = oldNode[i] + #else: + # result = usenewNode if oldPos + 1 == oldLength: for i in pos..newPos: - kxi.addPatch(pkAppend, current, nil, newNode[i], nil) + kxi.addPatch(pkAppend, current, nil, newNode[i]) + result = usenewNode else: let before = current.childNodes[oldPos + 1] for i in pos..newPos: - kxi.addPatch(pkInsertBefore, current, before, newNode[i], nil) + kxi.addPatch(pkInsertBefore, current, before, newNode[i]) + result = usenewNode # 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) + kxi.addPatch(pkRemove, current, current.childNodes[i], nil) + result = usenewNode of different: detach(oldNode) - kxi.addPatch(pkReplace, parent, current, newNode, nil) + kxi.addPatch(pkReplace, parent, current, newNode) of usenewNode: doAssert(false, "eq returned usenewNode") when defined(stats): dec kxi.recursion @@ -542,7 +470,11 @@ proc applyComponents(kxi: KaraxInstance) = 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): + if x.realDomImpl != nil: + let current = kxi.components[i].current + let parent = kxi.components[i].parent + kxi.addPatch(pkReplace, parent, current, x) + elif x.changedImpl != nil and x.changedImpl(x, newNode): when defined(karaxDebug): echo "Component ", newNode.text, " did change" let current = kxi.components[i].current @@ -555,25 +487,24 @@ proc applyComponents(kxi: KaraxInstance) = x.renderedVersion = x.version if oldExpanded == nil: detach(x) - kxi.addPatch(pkReplace, parent, current, x.expanded, nil) + kxi.addPatch(pkReplace, parent, current, x.expanded) 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" + let res = diff(x.expanded, oldExpanded, parent, current, kxi) + if res == usenewNode: + when defined(karaxDebug): + echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)" + discard "diff created a patchset for us, so this is fine" + elif res != different: + when defined(karaxDebug): + echo "Component ", newNode.text, ": re-render triggered no DOM change whatsoever" + x.expanded = oldExpanded + assert oldExpanded.dom != nil, "old expanded.dom is nil" + else: + when defined(karaxDebug): + echo "Component ", newNode.text, ": re-render triggered a DOM change (case B)" + assert x.expanded.dom != nil, "expanded.dom is nil" inc i setLen(kxi.components, 0) @@ -588,7 +519,7 @@ when defined(stats): proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) = detach(parent[position]) let current = parent.dom - kxi.addPatch(pkRemove, current, current.childNodes[position], nil, nil) + kxi.addPatch(pkRemove, current, current.childNodes[position], nil) parent.delete(position) applyPatch(kxi) doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) @@ -596,11 +527,11 @@ proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) = proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) = let current = parent.dom if position >= parent.len: - kxi.addPatch(pkAppend, current, nil, kid, nil) + kxi.addPatch(pkAppend, current, nil, kid) parent.add(kid) else: let before = current.childNodes[position] - kxi.addPatch(pkInsertBefore, current, before, kid, nil) + kxi.addPatch(pkInsertBefore, current, before, kid) parent.insert(kid, position) applyPatch(kxi) doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) @@ -608,7 +539,7 @@ proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) = proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) = let olddom = oldNode.dom doAssert olddom != nil - diff(newNode, oldNode, nil, olddom, kxi) + discard 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: @@ -619,44 +550,20 @@ proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) = 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) + let newtree = kxi.renderer() inc kxi.runCount newtree.id = kxi.rootId kxi.toFocus = nil if kxi.currentTree == nil: - let asdom = toDom(newtree, useAttachedNode = true, kxi) + let asdom = vnodeToDom(newtree, kxi) replaceById(kxi.rootId, asdom) else: doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) let olddom = document.getElementById(kxi.rootId) - diff(newtree, kxi.currentTree, nil, olddom, kxi) + discard diff(newtree, kxi.currentTree, nil, olddom, kxi) + #kout cstring"patch len ", patches.len when defined(profileKarax): echo "<<<<<<<<<<<<<<" echa reasons @@ -670,17 +577,20 @@ proc dodraw(kxi: KaraxInstance) = doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) if not kxi.postRenderCallback.isNil: - kxi.postRenderCallback(rdata) + kxi.postRenderCallback() # now that it's part of the DOM, give it the focus: if kxi.toFocus != nil: kxi.toFocus.focus() - kxi.rendering = false + kxi.renderId = 0 when defined(stats): kxi.recursion = 0 var total = 0 echo "depth ", depth(kxi.currentTree, total), " total ", total +proc reqFrame(callback: proc()): int {.importc: "window.requestAnimationFrame".} +proc cancelFrame(id: int) {.importc: "window.cancelAnimationFrame".} + proc redraw*(kxi: KaraxInstance = kxi) = # we buffer redraw requests: when false: @@ -698,49 +608,26 @@ 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.} = +proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT", + clientPostRenderCallback: proc () = nil): KaraxInstance {.discardable.} = ## Setup Karax. Usually the return value can be ignored. - if document.getElementById(root).isNil: - let msg = "Could not find a
with id=" & root & - ". Karax needs it as its rendering target." - raise newException(Exception, $msg) - result = KaraxInstance(rootId: root, renderer: renderer, postRenderCallback: clientPostRenderCallback, patches: newSeq[Patch](60), patchesV: newSeq[PatchV](30), - components: @[], - surpressRedraws: false, - byId: newJDict[cstring, VNode](), - orphans: newJDict[cstring, bool]()) + components: @[]) 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.} = +proc setInitializer*(renderer: proc (): VNode, root: cstring = "ROOT", + clientPostRenderCallback: proc () = 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]()) + surpressRedraws: true) kxi = result window.onload = init @@ -768,21 +655,16 @@ proc addEventHandler*(n: VNode; k: EventKind; action: proc(); 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 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 -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, @@ -792,7 +674,7 @@ proc setupErrorHandler*() = 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) + kout(x) return true # suppressErrorAlert {.pop.} @@ -800,35 +682,9 @@ proc prepend(parent, kid: Element) = parent.insertBefore(kid, parent.firstChild) proc loadScript*(jsfilename: cstring; kxi: KaraxInstance = kxi) = + let body = document.getElementById("body") let s = document.createElement("script") s.setAttr "type", "text/javascript" s.setAttr "src", jsfilename - document.body.prepend(s) + body.prepend(s) redraw(kxi) - -proc runLater*(action: proc(); later = 400): Timeout {.discardable.} = - proc wrapper() = - action() - redraw() - result = setTimeout(wrapper, later) - -proc setInputText*(n: VNode; s: cstring) = - ## Sets the text of input elements. - n.text = s - if n.dom != nil: n.dom.value = s - -proc getInputText*(n: VNode): cstring = - if n.dom != nil: - result = n.dom.value - -proc toChecked*(checked: bool): cstring = - (if checked: cstring"checked" else: cstring(nil)) - -proc toDisabled*(disabled: bool): cstring = - (if disabled: cstring"disabled" else: cstring(nil)) - -proc toSelected*(selected: bool): cstring = - (if selected: cstring"selected" else: cstring(nil)) - -proc vnodeToDom*(n: VNode; kxi: KaraxInstance = nil): Node = - result = toDom(n, useAttachedNode = false, kxi) diff --git a/karax/karaxdsl.nim b/src/karaxdsl.nim similarity index 83% rename from karax/karaxdsl.nim rename to src/karaxdsl.nim index 451632c..dd480e1 100644 --- a/karax/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -1,46 +1,25 @@ -import macros, vdom, compact, kbase +import macros, karax, vdom, compact 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 nnkIdent: + result = $n.ident of nnkAccQuoted: result = "" for i in 0..