diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..5e6c415
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,31 @@
+# This is a basic workflow to help you get started with Actions
+
+name: CI
+
+# Controls when the action will run.
+on:
+ # Triggers the workflow on push or pull request events but only for the master branch
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+# A workflow run is made up of one or more jobs that can run sequentially or in parallel
+jobs:
+ # This workflow contains a single job called "build"
+ build:
+ # The type of runner that the job will run on
+ runs-on: ubuntu-latest
+ container: nimlang/nim
+ # Steps represent a sequence of tasks that will be executed as part of the job
+ steps:
+ # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
+ - uses: actions/checkout@v2
+ # Runs a single command using the runners shell
+ - name: Tests
+ run: |
+ nimble develop -y
+ nim c -r tests/tester.nim
diff --git a/.gitignore b/.gitignore
index 67d9b34..842e140 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,7 @@
nimcache/
+/tests/tester
+karax/tools/karun
+*.code-workspace
+*.exe
+app.js
+app.html
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..ac49796
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2017 Xored Software, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/docs/benchmark.png b/docs/benchmark.png
new file mode 100644
index 0000000..3949ad5
Binary files /dev/null and b/docs/benchmark.png differ
diff --git a/docs/docs.rst b/docs/docs.rst
new file mode 100644
index 0000000..3eec1a4
--- /dev/null
+++ b/docs/docs.rst
@@ -0,0 +1,58 @@
+
+Virtual DOM
+===========
+
+The virtual dom is in the ``vdom`` module.
+
+The ``VNodeKind`` enum describes every tag that the HTML 5 spec includes
+and some extensions that allow for an efficient component system.
+
+These extensions are:
+
+``VNodeKind.int``
+ The node has a single integer field.
+
+``VNodeKind.bool``
+ The node has a single boolean field.
+
+``VNodeKind.vthunk``
+ The node is a `virtual thunk`:idx:. This means there is a
+ function attached to it that produces the ``VNode`` structure
+ on demand.
+
+``VNodeKind.dthunk``
+ The node is a `DOM thunk`:idx:. This means there is a
+ function attached to it that produces the ``Node`` DOM structure
+ on demand.
+
+For convenience of the resulting Nim DSL these tags have enum names
+that differ from their HTML equivalent:
+
+================= =======================================================
+Enum value HTML
+================= =======================================================
+``tdiv`` ``div`` (changed because ``div`` is a keyword in Nim)
+``italic`` ``i``
+``bold`` ``b``
+``strikethrough`` ``s``
+``underlined`` ``u``
+================= =======================================================
+
+
+A virtual dom node also has a special field set called ``key``, an integer
+that can be used as a data model specific key/id. It can be accessed in event
+handlers to change the data model. See the todoapp for an example of how to
+use it.
+
+**Note**: This is not a hint for the DOM diff algorithm, multipe nodes can
+all have the same key value.
+
+
+Event system
+============
+
+Karax does not abstract over the event system the DOM offers much: The same
+``Event`` structure is used. Every callback has the
+signature ``proc (ev: Event; n: VNode)`` or empty ``proc ()``.
+
+
diff --git a/examples/button.nim b/examples/button.nim
new file mode 100644
index 0000000..d86fa82
--- /dev/null
+++ b/examples/button.nim
@@ -0,0 +1,15 @@
+include karax / prelude
+
+var lines: seq[kstring] = @[]
+
+proc createDom(): VNode =
+ result = buildHtml(tdiv):
+ button:
+ text "Say hello!"
+ proc onclick(ev: Event; n: VNode) =
+ lines.add "Hello simulated universe"
+ for x in lines:
+ tdiv:
+ text x
+
+setRenderer createDom
diff --git a/examples/buttonlambda.nim b/examples/buttonlambda.nim
new file mode 100644
index 0000000..2ddadff
--- /dev/null
+++ b/examples/buttonlambda.nim
@@ -0,0 +1,14 @@
+include karax / prelude
+from future import `=>`
+
+var lines: seq[kstring] = @[]
+
+proc createDom(): VNode =
+ result = buildHtml(tdiv):
+ button(onclick = () => lines.add "Hello simulated universe"):
+ text "Say hello!"
+ for x in lines:
+ tdiv:
+ text x
+
+setRenderer createDom
diff --git a/examples/carousel/carousel.html b/examples/carousel/carousel.html
new file mode 100644
index 0000000..500e73a
--- /dev/null
+++ b/examples/carousel/carousel.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Carousel app
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/carousel/carousel.nim b/examples/carousel/carousel.nim
new file mode 100644
index 0000000..c2db56c
--- /dev/null
+++ b/examples/carousel/carousel.nim
@@ -0,0 +1,69 @@
+## This demo shows how you can develop your own stateful components with Karax.
+
+import vdom, vstyles, karax, karaxdsl, jdict, jstrutils, kdom
+
+type
+ Carousel = ref object of VComponent
+ counter: int
+ cntdown: int
+ timer: TimeOut
+ list: seq[cstring]
+
+const ticksUntilChange = 5
+
+var
+ images: seq[cstring] = @[cstring"a", "b", "c", "d"]
+
+proc render(x: VComponent): VNode =
+ let self = Carousel(x)
+
+ proc docount() =
+ dec self.cntdown
+ if self.cntdown == 0:
+ self.counter = (self.counter + 1) mod self.list.len
+ self.cntdown = ticksUntilChange
+ else:
+ self.timer = setTimeout(docount, 30)
+ markDirty(self)
+ redraw()
+
+ proc onclick(ev: Event; n: VNode) =
+ if self.timer != nil:
+ clearTimeout(self.timer)
+ self.timer = setTimeout(docount, 30)
+
+ let col =
+ case self.counter
+ of 0: cstring"#4d4d4d"
+ of 1: cstring"#ff00ff"
+ of 2: cstring"#00ffff"
+ of 3: cstring"#ffff00"
+ else: cstring"red"
+ result = buildHtml(tdiv()):
+ text self.list[self.counter]
+ button(onclick = onclick):
+ text "Next"
+ tdiv(style = style(StyleAttr.color, col)):
+ text "This changes its color."
+ if self.cntdown != ticksUntilChange:
+ text &self.cntdown
+
+proc carousel(): Carousel =
+ result = newComponent(Carousel, render)
+ result.list = images
+ result.cntdown = ticksUntilChange
+
+proc createDom(): VNode =
+ result = buildHtml(table):
+ tr:
+ td:
+ carousel()
+ td:
+ carousel()
+ tr:
+ td:
+ carousel()
+ td:
+ carousel()
+
+setRenderer createDom
diff --git a/examples/carousel/carousel.nims b/examples/carousel/carousel.nims
new file mode 100644
index 0000000..cef2dec
--- /dev/null
+++ b/examples/carousel/carousel.nims
@@ -0,0 +1 @@
+--path: "../../karax"
diff --git a/examples/config.nims b/examples/config.nims
new file mode 100644
index 0000000..76ed4d9
--- /dev/null
+++ b/examples/config.nims
@@ -0,0 +1 @@
+--path: ".."
diff --git a/examples/hellostyle.nim b/examples/hellostyle.nim
new file mode 100644
index 0000000..716c2f0
--- /dev/null
+++ b/examples/hellostyle.nim
@@ -0,0 +1,17 @@
+include karax / prelude
+import karax / vstyles
+
+proc createDom(): VNode =
+ result = buildHtml(tdiv):
+ tdiv(style = style(StyleAttr.color, "red".cstring)):
+ text "red"
+ tdiv(style = style(StyleAttr.color, "blue")):
+ text "blue"
+ # explicit `kstring` required for varargs overload
+ tdiv(style = style((fontStyle, "italic".kstring), (color, "orange".kstring))):
+ text "italic orange"
+ # can use a string directly
+ tdiv(style = "font-style: oblique; color: pink".toCss):
+ text "oblique pink"
+
+setRenderer createDom
diff --git a/examples/hellouniverse.nim b/examples/hellouniverse.nim
new file mode 100644
index 0000000..8343d07
--- /dev/null
+++ b/examples/hellouniverse.nim
@@ -0,0 +1,12 @@
+include karax / prelude
+import random
+
+proc createDom(): VNode =
+ result = buildHtml(tdiv):
+ if rand(100) <= 50:
+ text "Hello World!"
+ else:
+ text "Hello Universe"
+
+randomize()
+setRenderer createDom
diff --git a/examples/helloworld.nim b/examples/helloworld.nim
new file mode 100644
index 0000000..76c43db
--- /dev/null
+++ b/examples/helloworld.nim
@@ -0,0 +1,7 @@
+include karax / prelude
+
+proc createDom(): VNode =
+ result = buildHtml(tdiv):
+ text "Hello World!"
+
+setRenderer createDom
diff --git a/examples/login.nim b/examples/login.nim
new file mode 100644
index 0000000..0350e23
--- /dev/null
+++ b/examples/login.nim
@@ -0,0 +1,46 @@
+include karax / prelude
+from sugar import `=>`
+import karax / errors
+
+proc loginField(desc, field, class: kstring;
+ validator: proc (field: kstring): proc ()): VNode =
+ result = buildHtml(tdiv):
+ label(`for` = field):
+ text desc
+ input(class = class, id = field, onchange = validator(field))
+
+# some consts in order to prevent typos:
+const
+ username = kstring"username"
+ password = kstring"password"
+
+proc validateNotEmpty(field: kstring): proc () =
+ result = proc () =
+ let x = getVNodeById(field)
+ if x.text == "":
+ errors.setError(field, field & " must not be empty")
+ else:
+ errors.setError(field, "")
+
+var loggedIn: bool
+
+proc loginDialog(): VNode =
+ result = buildHtml(tdiv):
+ if not loggedIn:
+ loginField("Name: ", username, "input", validateNotEmpty)
+ loginField("Password: ", password, "password", validateNotEmpty)
+ button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()):
+ text "Login"
+ p:
+ text errors.getError(username)
+ p:
+ text errors.getError(password)
+ else:
+ p:
+ text "You are now logged in."
+
+setError username, username & " must not be empty"
+setError password, password & " must not be empty"
+
+when not declared(toychat):
+ setRenderer loginDialog
diff --git a/examples/mediaplayer/mediaplayer.nim b/examples/mediaplayer/mediaplayer.nim
new file mode 100644
index 0000000..c84b68b
--- /dev/null
+++ b/examples/mediaplayer/mediaplayer.nim
@@ -0,0 +1,42 @@
+
+import karax / [karax, karaxdsl, vdom, kdom, compact]
+
+const
+ Play = 0
+ Big = 1
+ Small = 2
+ Normal = 3
+
+proc paused(n: Node): bool {.importcpp: "#.paused".}
+proc play(n: Node) {.importcpp.}
+proc pause(n: Node) {.importcpp.}
+proc `width=`(n: Node, w: int) {.importcpp: "#.width = #".}
+
+proc mplayer*(id, resource: cstring): VNode {.compact.} =
+ proc handler(ev: Event; n: VNode) =
+ let myVideo = document.getElementById(id)
+ case n.index
+ of Play:
+ if myVideo.paused:
+ myVideo.play()
+ else:
+ myVideo.pause()
+ of Big: myVideo.width = 560
+ of Small: myVideo.width = 320
+ of Normal: myVideo.width = 420
+ else: discard
+
+ result = buildHtml(tdiv):
+ button(onclick=handler, index=Play):
+ text "Play/Pause"
+ button(onclick=handler, index=Big):
+ text "Big"
+ button(onclick=handler, index=Small):
+ text "Small"
+ button(onclick=handler, index=Normal):
+ text "Normal"
+ br()
+ br()
+ video(id=id, width="420"):
+ source(src=resource, `type`="video/mp4"):
+ text "Your browser does not support HTML5 video."
diff --git a/examples/mediaplayer/playerapp.html b/examples/mediaplayer/playerapp.html
new file mode 100644
index 0000000..a45a2e8
--- /dev/null
+++ b/examples/mediaplayer/playerapp.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Player app
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/mediaplayer/playerapp.nim b/examples/mediaplayer/playerapp.nim
new file mode 100644
index 0000000..acb2e68
--- /dev/null
+++ b/examples/mediaplayer/playerapp.nim
@@ -0,0 +1,21 @@
+## Example app that shows how to write and embed a custom component.
+
+include karax/prelude
+import mediaplayer
+
+const url = "https://www.w3schools.com/html/mov_bbb.mp4"
+
+proc createDom(): VNode =
+ result = buildHtml(table):
+ tr:
+ td:
+ mplayer("vid1", url)
+ td:
+ mplayer("vid2", url)
+ tr:
+ td:
+ mplayer("vid3", url)
+ td:
+ mplayer("vid4", url)
+
+setRenderer createDom
diff --git a/examples/mediaplayer/playerapp.nims b/examples/mediaplayer/playerapp.nims
new file mode 100644
index 0000000..cf2eb9e
--- /dev/null
+++ b/examples/mediaplayer/playerapp.nims
@@ -0,0 +1 @@
+--path: "../.."
diff --git a/examples/scrollapp/scrollapp.html b/examples/scrollapp/scrollapp.html
new file mode 100644
index 0000000..44d657e
--- /dev/null
+++ b/examples/scrollapp/scrollapp.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Todo app
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/scrollapp/scrollapp.nim b/examples/scrollapp/scrollapp.nim
new file mode 100644
index 0000000..4490447
--- /dev/null
+++ b/examples/scrollapp/scrollapp.nim
@@ -0,0 +1,25 @@
+## Example that shows how to accomplish an "infinitely scrolling" app.
+
+include karax/prelude
+import karax / [jstrutils, kdom, vstyles]
+
+var entries: seq[cstring] = @[]
+for i in 1..500:
+ entries.add(cstring("Entry ") & &i)
+
+proc scrollEvent(ev: Event; n: VNode) =
+ let d = n.dom
+ if d != nil and inViewport(d.lastChild):
+ # "load" more data:
+ for i in 1..50:
+ entries.add(cstring("Loaded Entry ") & &i)
+
+proc createDom(): VNode =
+ result = buildHtml():
+ tdiv(onscroll=scrollEvent, style=style(
+ (StyleAttr.height, cstring"400px"), (StyleAttr.overflow, cstring"scroll"))):
+ for x in entries:
+ tdiv:
+ text x
+
+setRenderer createDom
diff --git a/examples/scrollapp/scrollapp.nims b/examples/scrollapp/scrollapp.nims
new file mode 100644
index 0000000..cf2eb9e
--- /dev/null
+++ b/examples/scrollapp/scrollapp.nims
@@ -0,0 +1 @@
+--path: "../.."
diff --git a/examples/todoapp/todoapp.html b/examples/todoapp/todoapp.html
index 726070a..ab3daa7 100644
--- a/examples/todoapp/todoapp.html
+++ b/examples/todoapp/todoapp.html
@@ -1,19 +1,16 @@
-
- Todo app
-
-
+
+ Todo app
+
+
-
+
-
+
-
-
+
-
+
diff --git a/examples/todoapp/todoapp.nim b/examples/todoapp/todoapp.nim
index 23840f7..e3720fd 100644
--- a/examples/todoapp/todoapp.nim
+++ b/examples/todoapp/todoapp.nim
@@ -1,116 +1,152 @@
-import vdom, karax, karaxdsl, jstrutils
+import karax / [vdom, karax, karaxdsl, jstrutils, compact, localstorage]
type
Filter = enum
all, active, completed
var
- entries: seq[(cstring, bool)]
selectedEntry = -1
filter: Filter
+ entriesLen: int
+ doneswitch = true
+const
+ contentSuffix = cstring"content"
+ completedSuffix = cstring"completed"
+ lenSuffix = cstring"entriesLen"
+
+proc getEntryContent(pos: int): cstring =
+ result = getItem(&pos & contentSuffix)
+ if result == cstring"null":
+ result = nil
+
+proc isCompleted(pos: int): bool =
+ var value = getItem(&pos & completedSuffix)
+ result = value == cstring"true"
+
+proc setEntryContent(pos: int, content: cstring) =
+ setItem(&pos & contentSuffix, content)
+
+proc markAsCompleted(pos: int, completed: bool) =
+ setItem(&pos & completedSuffix, &completed)
+
+proc addEntry(content: cstring, completed: bool) =
+ setEntryContent(entriesLen, content)
+ markAsCompleted(entriesLen, completed)
+ inc entriesLen
+ setItem(lenSuffix, &entriesLen)
+
+proc updateEntry(pos: int, content: cstring, completed: bool) =
+ setEntryContent(pos, content)
+ markAsCompleted(pos, completed)
proc onTodoEnter(ev: Event; n: VNode) =
- entries.add((n.value, false))
- n.value = ""
+ if n.value.strip() != "":
+ addEntry(n.value, false)
+ n.value = ""
proc removeHandler(ev: Event; n: VNode) =
- entries[n.key] = (cstring(nil), false)
+ updateEntry(n.index, cstring(nil), false)
proc editHandler(ev: Event; n: VNode) =
- selectedEntry = n.key
+ selectedEntry = n.index
proc focusLost(ev: Event; n: VNode) = selectedEntry = -1
proc editEntry(ev: Event; n: VNode) =
- entries[n.key][0] = n.value
+ setEntryContent(n.index, n.value)
selectedEntry = -1
proc toggleEntry(ev: Event; n: VNode) =
- let id = n.key
- entries[id][1] = not entries[id][1]
+ let id = n.index
+ markAsCompleted(id, not isCompleted(id))
proc onAllDone(ev: Event; n: VNode) =
- entries = @[]
- selectedEntry = -1
-
+ for i in 0..
+
+
+
+ Karax AJAX Test
+
+
+
+
+
+
+
+
diff --git a/experiments/ajaxtest.nim b/experiments/ajaxtest.nim
new file mode 100644
index 0000000..720b6b2
--- /dev/null
+++ b/experiments/ajaxtest.nim
@@ -0,0 +1,6 @@
+import kdom, kajax
+
+proc cb(httpStatus: int, response: cstring) =
+ echo "Worked!"
+
+ajaxGet("https://httpbin.org/get", @[], cb)
diff --git a/experiments/config.nims b/experiments/config.nims
new file mode 100644
index 0000000..86673aa
--- /dev/null
+++ b/experiments/config.nims
@@ -0,0 +1,2 @@
+--path: "../karax"
+--path: ".."
diff --git a/experiments/echartstest.html b/experiments/echartstest.html
new file mode 100644
index 0000000..8c9b225
--- /dev/null
+++ b/experiments/echartstest.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Karax ECharts Test
+
+
+
+
+
+
+
+
+
diff --git a/experiments/echartstest.nim b/experiments/echartstest.nim
new file mode 100644
index 0000000..950c6ce
--- /dev/null
+++ b/experiments/echartstest.nim
@@ -0,0 +1,58 @@
+import karax / [kbase, vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, jjson]
+
+type
+ Views = enum
+ Customers, Products
+
+var
+ currentView = Customers
+
+type
+ EChart* = ref object
+
+proc echartsInit(n: Element): EChart {.importc: "echarts.init".}
+proc setOption(x: EChart; option: JsonNode) {.importcpp.}
+
+proc postRender(data: RouterData) =
+ if currentView == Products:
+ let myChart = echartsInit(kdom.getElementById("echartSection"))
+ # specify chart configuration item and data
+ let option = %*{
+ "title": {
+ "text": "ECharts entry example"
+ },
+ "tooltip": {},
+ "legend": {
+ "data": ["Sales"]
+ },
+ "xAxis": {
+ "data": ["shirt","cardign","chiffon shirt","pants","heels","socks"]
+ },
+ "yAxis": {},
+ "series": [{
+ "name": "Sales",
+ "type": "bar",
+ "data": [5, 20, 36, 10, 10, 20]
+ }]
+ }
+ myChart.setOption(option)
+
+proc createDom(data: RouterData): VNode =
+ let hash = data.hashPart
+ if hash == cstring"#/Products": currentView = Products
+ else: currentView = Customers
+
+ result = buildHtml(tdiv):
+ ul(class = "tabs"):
+ for v in low(Views)..high(Views):
+ li:
+ a(href = "#/" & $v):
+ text cstring($v)
+
+ if currentView == Products:
+ tdiv(id = "echartSection", style = style((width, kstring"600px"), (height, kstring"400px")))
+ tdiv:
+ text "other section"
+
+setRenderer createDom, "ROOT", postRender
+setForeignNodeId "echartSection"
diff --git a/experiments/example.html b/experiments/example.html
deleted file mode 100644
index 35b9837..0000000
--- a/experiments/example.html
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
- Todo app
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/experiments/example.nim b/experiments/example.nim
deleted file mode 100644
index 0ea805e..0000000
--- a/experiments/example.nim
+++ /dev/null
@@ -1,135 +0,0 @@
-
-import vdom, karax, karaxdsl, jdict, jstrutils
-
-type
- Filter = enum
- all, active, completed
-
-var
- entries: seq[(cstring, bool)]
- selectedEntry = -1
- filter: Filter
-
-proc onTodoEnter(ev: Event; n: VNode) =
- entries.add((n.value, false))
- n.value = ""
-
-proc removeHandler(ev: Event; n: VNode) =
- let id = suffixAsInt(n.id, "remove:")
- entries[id] = (cstring(nil), false)
-
-proc editHandler(ev: Event; n: VNode) =
- let id = suffixAsInt(n.id, "edit:")
- selectedEntry = id
-
-when defined(usecache):
- var entryCache = newJDict[int, VNode]()
-
-proc focusLost(ev: Event; n: VNode) = selectedEntry = -1
-
-proc editEntry(ev: Event; n: VNode) =
- let id = suffixAsInt(n.id, "todo-edit:")
- entries[id][0] = n.value
- selectedEntry = -1
-
-proc toggleEntry(ev: Event; n: VNode) =
- let id = suffixAsInt(n.id, "toggle:")
- entries[id][1] = not entries[id][1]
-
-proc onAllDone(ev: Event; n: VNode) =
- entries = @[]
- selectedEntry = -1
-
-proc clearCompleted(ev: Event, n: VNode) =
- for i in 0.. 0:
+ let p = prevSelected #selected.value
+ if p != nil:
+ if isFirstName:
+ p.firstname = v
+ else:
+ p.lastname = v
+ unselect()
+ errmsg <- ""
+ else:
+ errmsg <- "name must not be empty"
+
+var inpFirstname = toUI(true)
+var inpLastname = toUI(false)
+
+proc adaptFocus(def = false): RBool =
+ result = RBool()
+ result.value = def
+ when false:
+ result.subscribe proc (hasFocus: bool) =
+ if not hasFocus:
+ unselect()
+
+var focusA = adaptFocus()
+var focusB = adaptFocus()
+
+proc styler(): VStyle =
+ result = style(
+ (StyleAttr.position, cstring"relative"),
+ (StyleAttr.paddingLeft, cstring"10px"),
+ (StyleAttr.paddingRight, cstring"5px"),
+ (StyleAttr.height, cstring"30px"),
+ (StyleAttr.lineHeight, cstring"30px"),
+ (StyleAttr.border, cstring"solid 8px " & (if focusA.value: cstring"red" else: cstring"black")),
+ (StyleAttr.fontSize, cstring"12px"),
+ (StyleAttr.fontWeight, cstring"600")
+ )
+
+var clicks = 0
+
+proc renderUser(u: User): VNode {.track.} =
+ result = buildHtml(tdiv):
+ if u.selected:
+ inpFirstname <- u.firstname
+ tdiv:
+ textInput inpFirstname, focusA
+ inpLastname <- u.lastname
+ tdiv:
+ textInput inpLastname, focusB
+ else:
+ button:
+ text "..."
+ proc onclick(ev: Event; n: VNode) =
+ select(u)
+ text u.firstname & " " & u.lastname
+ button:
+ text "(x)"
+ proc onclick(ev: Event; n: VNode) =
+ gu.deleteElem(u)
+
+proc main(gu: RSeq[User]): VNode =
+ result = buildHtml(tdiv):
+ tdiv:
+ button:
+ text "Add User"
+ proc onclick(ev: Event; n: VNode) =
+ inc clicks
+ gu.add User(rawFirstname: "Added", rawLastname: &clicks)
+ tdiv:
+ text errmsg
+ vmapIt(gu, tdiv, renderUser(it))
+
+proc init(): VNode = main(gu)
+
+setInitializer(init)
diff --git a/experiments/oldwidgets.nim b/experiments/oldwidgets.nim
new file mode 100644
index 0000000..c947d7f
--- /dev/null
+++ b/experiments/oldwidgets.nim
@@ -0,0 +1,251 @@
+var
+ linkCounter: int
+
+proc link*(id: int): VNode =
+ result = newVNode(VNodeKind.anchor)
+ result.setAttr("href", "#")
+ inc linkCounter
+ result.setAttr("id", $linkCounter & ":" & $id)
+
+proc link*(action: EventHandler): VNode =
+ result = newVNode(VNodeKind.anchor)
+ result.setAttr("href", "#")
+ result.setOnclick action
+
+when false:
+ proc button*(caption: cstring; action: EventHandler; disabled=false): VNode =
+ result = newVNode(VNodeKind.button)
+ result.add text(caption)
+ if action != nil:
+ result.setOnClick action
+ if disabled:
+ result.setAttr("disabled", "true")
+
+proc select*(choices: openarray[cstring]): VNode =
+ result = newVNode(VNodeKind.select)
+ var i = 0
+ for c in choices:
+ result.add tree(VNodeKind.option, [(cstring"value", toCstr(i))], text(c))
+ inc i
+
+proc select*(choices: openarray[(int, cstring)]): VNode =
+ result = newVNode(VNodeKind.select)
+ for c in choices:
+ result.add tree(VNodeKind.option, [(cstring"value", toCstr(c[0]))], text(c[1]))
+
+var radioCounter: int
+
+proc radio*(choices: openarray[(int, cstring)]): VNode =
+ result = newVNode(VNodeKind.fieldset)
+ var i = 0
+ inc radioCounter
+ for c in choices:
+ let id = cstring"radio_" & c[1] & toCstr(i)
+ var kid = tree(VNodeKind.input, [(cstring"type", cstring"radio"),
+ (cstring"id", id), (cstring"name", cstring"radio" & toCStr(radioCounter)),
+ (cstring"value", toCStr(c[0]))])
+ if i == 0:
+ kid.setAttr(cstring"checked", cstring"checked")
+ var lab = tree(VNodeKind.label, [(cstring"for", id)], text(c[1]))
+ kid.add lab
+ result.add kid
+ inc i
+
+proc tag*(kind: VNodeKind; id=cstring(nil), class=cstring(nil)): VNode =
+ result = newVNode(kind)
+ result.id = id
+ result.class = class
+
+proc tdiv*(id=cstring(nil), class=cstring(nil)): VNode = tag(VNodeKind.tdiv, id, class)
+proc span*(id=cstring(nil), class=cstring(nil)): VNode = tag(VNodeKind.span, id, class)
+
+proc valueAsInt*(e: Node): int = parseInt(e.value)
+
+proc th*(s: cstring): VNode =
+ result = newVNode(VNodeKind.th)
+ result.add text(s)
+
+proc td*(s: string): VNode =
+ result = newVNode(VNodeKind.td)
+ result.add text(s)
+
+proc td*(s: VNode): VNode =
+ result = newVNode(VNodeKind.td)
+ result.add s
+
+proc td*(class: cstring; s: VNode): VNode =
+ result = newVNode(VNodeKind.td)
+ result.add s
+ result.class = class
+
+proc table*(class=cstring(nil), kids: varargs[VNode]): VNode =
+ result = tag(VNodeKind.table, nil, class)
+ for k in kids: result.add k
+
+proc tr*(kids: varargs[VNode]): VNode =
+ result = newVNode(VNodeKind.tr)
+ for k in kids:
+ if k.kind in {VNodeKind.td, VNodeKind.th}:
+ result.add k
+ else:
+ result.add td(k)
+
+proc suffix*(s, prefix: cstring): cstring =
+ if s.startsWith(prefix):
+ result = s.substr(prefix.len)
+ else:
+ kout(cstring"bug! " & s & cstring" does not start with " & prefix)
+
+proc suffixAsInt*(s, prefix: cstring): int = parseInt(suffix(s, prefix))
+
+#proc ceil(f: float): int {.importc: "Math.ceil", nodecl.}
+
+proc realtimeInput*(val: cstring; action: EventHandler): VNode =
+ var timer: Timeout
+ proc onkeyup(ev: Event; n: VNode) =
+ proc wrapper() = keyeventBody()
+
+ if timer != nil: clearTimeout(timer)
+ timer = setTimeout(wrapper, 400)
+ result = tree(VNodeKind.input, [(cstring"type", cstring"text")])
+ result.value = val
+ result.addEventListener(EventKind.onkeyup, onkeyup)
+
+proc enterInput*(id, val: cstring; action: EventHandler): VNode =
+ proc onkeyup(ev: Event; n: VNode) =
+ if ev.keyCode == 13: keyeventBody()
+
+ result = tree(VNodeKind.input, [(cstring"type", cstring"text")])
+ result.id = id
+ result.value = val
+ result.addEventListener(EventKind.onkeyup, onkeyup)
+
+proc setOnEnter*(n: VNode; action: EventHandler) =
+ proc onkeyup(ev: Event; n: VNode) =
+ if ev.keyCode == 13: keyeventBody()
+ n.addEventListener(EventKind.onkeyup, onkeyup)
+
+proc setOnscroll*(action: proc(min, max: VKey; diff: int)) =
+ var oldY = window.pageYOffset
+
+ proc wrapper(ev: Event) =
+ let dir = window.pageYOffset - oldY
+ if dir == 0: return
+
+ var a = VKey high(int)
+ var b = VKey 0
+ var h, count: int
+ document.visibleKeys(a, b, h, count)
+ let avgh = h / count
+ let diff = toInt(dir.float / avgh)
+ if diff != 0:
+ oldY = window.pageYOffset
+ action(a, b, diff)
+ redraw()
+
+ document.addEventListener("scroll", wrapper)
+
+when false:
+ var plugins {.exportc.}: seq[(string, proc())] = @[]
+
+ proc onInput(val: cstring) =
+ kout val
+ if val == "dyn":
+ kout(plugins.len)
+ if plugins.len > 0:
+ plugins[0][1]()
+
+var
+ images: seq[cstring] = @[cstring"a", "b", "c", "d"]
+
+proc carousel*(): VNode =
+ var currentIndex = 0
+
+ proc next(ev: Event; n: VNode) =
+ currentIndex = (currentIndex + 1) mod images.len
+
+ proc prev(ev: Event; n: VNode) =
+ currentIndex = (currentIndex - 1) mod images.len
+
+ result = buildHtml(tdiv):
+ text images[currentIndex]
+ button(onclick = next):
+ text "Next"
+ button(onclick = prev):
+ text "Previous"
+
+#proc targetElem*(e: Event): Element = cast[Element](e.target)
+
+#proc getElementsByClassName*(cls: cstring): seq[Element] {.importc:
+# "document.getElementsByClassName", nodecl.}
+#proc textContent(e: Node): cstring {.
+# importcpp: "#.textContent", nodecl.}
+
+proc isElementInViewport(el: Node; h: var int): bool =
+ let rect = el.getBoundingClientRect()
+ h = rect.bottom - rect.top
+ result = rect.top >= 0 and rect.left >= 0 and
+ rect.bottom <= clientHeight() and
+ rect.right <= clientWidth()
+
+proc visibleKeys(e: Node; a, b: var VKey; h, count: var int) =
+ # we only care about nodes that have a key:
+ var hh = 0
+ # do not recurse if there is a 'key' field already:
+ if e.key >= 0:
+ if isElementInViewport(e, hh):
+ inc count
+ inc h, hh
+ a = min(a, e.key)
+ b = max(b, e.key)
+ else:
+ for i in 0..
+
+
+
+
+ Refs
+
+
+
+
+
+
+
diff --git a/experiments/refs/demo.nim b/experiments/refs/demo.nim
new file mode 100644
index 0000000..a0db59f
--- /dev/null
+++ b/experiments/refs/demo.nim
@@ -0,0 +1,58 @@
+include karaxprelude
+import future, sequtils
+
+type
+ CustomRef = ref object of VComponent
+
+method onAttach(r: CustomRef) =
+ kout(cstring"custom ref attached")
+
+method onDetach(r: CustomRef) =
+ kout(cstring"custom ref detached")
+
+var
+ modelData = @[5, 2, 4]
+
+ refA = VComponent()
+ refB = VComponent()
+ refC = CustomRef()
+ refSeq = newSeq[VComponent]()
+
+proc onClick(ev: Event, n: VNode) =
+ modelData.add(0)
+
+proc showRefs() =
+ kout(refA)
+ kout(refB)
+ kout(refC)
+ for i in 0 ..< refSeq.len:
+ kout(refSeq[i].vnode)
+ #kout(refSeq.map(nref => nref.vnode))
+
+proc secureRefSlot(i: int): VComponent =
+ while refSeq.len <= i:
+ refSeq.add(VComponent())
+ result = refSeq[i]
+
+proc view(): VNode =
+ result = buildHtml():
+ tdiv:
+ button(onclick=onClick):
+ text "click me"
+ # storing refs to single elements is straightforward now
+ if modelData.len mod 2 == 0:
+ tdiv(nref=refA):
+ text "A"
+ tdiv(nref=refB):
+ text "B"
+ tdiv(nref=refC):
+ text "C"
+ # It's a bit more tricky when containers are involved:
+ for i, x in modelData.pairs:
+ tdiv(nref=secureRefSlot(i)):
+ text "List item: " & $x
+
+proc renderer(): VNode =
+ view()
+
+setRenderer(renderer, "ROOT", showRefs)
diff --git a/experiments/refs/demo.nims b/experiments/refs/demo.nims
new file mode 100644
index 0000000..9028ff1
--- /dev/null
+++ b/experiments/refs/demo.nims
@@ -0,0 +1 @@
+--path: "../../src"
diff --git a/experiments/scrollapp.html b/experiments/scrollapp.html
index ad3ce2f..8b909f1 100644
--- a/experiments/scrollapp.html
+++ b/experiments/scrollapp.html
@@ -10,7 +10,7 @@
-
+
diff --git a/experiments/scrollapp.nim b/experiments/scrollapp.nim
index 7b7ce8e..86a802f 100644
--- a/experiments/scrollapp.nim
+++ b/experiments/scrollapp.nim
@@ -1,17 +1,14 @@
-import vdom, karax, karaxdsl, jdict, jstrutils
+import vdom, karax, karaxdsl, jdict, jstrutils, compact
type
Filter = enum
all, active, completed
-const
- WindowSize = 50 # overestimating it does not harm
-
var
entries: seq[(cstring, bool)]
visibleA = 0
- visibleB = 10_000
+ visibleB = 10
selectedEntry = -1
filter: Filter
@@ -52,34 +49,15 @@ proc toChecked(checked: bool): cstring =
proc selected(v: Filter): cstring =
(if filter == v: cstring"selected" else: cstring(nil))
-when false:
- proc entry(id: int, d: cstring; completed, selected: bool): proc (): VNode =
- var lastid: type(id)
- var lastd: type(d)
- var lastCompleted: type(completed)
- var lastSelected: type(selected)
- proc inner(): VNode =
- if lastid != id or lastd != d or lastCompleted != completed or lastSelected != selected:
- result = logic(id, d, completed, selected)
- lastid = id
- lastd = d
- lastCompleted = completed
- lastSelected = selected
-
- result = inner
-
-proc lazyEntry(args: seq[VNode]): VNode =
- let id = args[0].intValue
- let d = args[1].text
- let completed = args[2].intValue != 0
- let selected = args[3].intValue != 0
+proc createEntry(id: int; d: cstring;
+ completed: bool, selected: bool): VNode {.component.} =
result = buildHtml(tr):
li(class=toClass(completed), key = id):
if not selected:
tdiv(class = "view"):
input(class = "toggle", `type` = "checkbox", checked = toChecked(completed),
onclick=toggleEntry, key = id)
- label(onDblClick=editHandler, key = id):
+ label(onclick=editHandler, key = id):
text d
button(class = "destroy", key = id, onclick = removeHandler)
else:
@@ -87,9 +65,6 @@ proc lazyEntry(args: seq[VNode]): VNode =
onfocusLost = focusLost,
onenter = editEntry, value = d, setFocus)
-proc createEntry(id: int; d: cstring; completed, selected: bool): VNode =
- result = thunk(lazyEntry, id, d, completed, selected)
-
proc myscroll(ev: Event; n: VNode) =
kout cstring"scrolling event here"
@@ -105,10 +80,6 @@ proc createDom(): VNode =
input(class = "toggle-all", `type` = "checkbox", name = "toggle")
label(`for` = "toggle-all", onclick = onAllDone):
text "Mark all as complete"
- tdiv(onscroll = myscroll, style = "overflow: visible; width: 0px; display: block; max-height: 1em"):
- for i in 0..4:
- tdiv:
- text entries[i][0]
var entriesCount = 0
var completedCount = 0
diff --git a/experiments/scrollapp_simple.nim b/experiments/scrollapp_simple.nim
new file mode 100644
index 0000000..cdd19cd
--- /dev/null
+++ b/experiments/scrollapp_simple.nim
@@ -0,0 +1,25 @@
+## Example that shows how to accomplish an "infinitely scrolling" app.
+
+include karax / prelude
+import karax / [kdom, vstyles]
+
+var entries: seq[cstring] = @[]
+for i in 1..500:
+ entries.add(cstring("Entry ") & &i)
+
+proc scrollEvent(ev: Event; n: VNode) =
+ let d = n.dom
+ if d != nil and inViewport(d.lastChild):
+ # "load" more data:
+ for i in 1..50:
+ entries.add(cstring("Loaded Entry ") & &i)
+
+proc createDom(): VNode =
+ result = buildHtml():
+ tdiv(onscroll=scrollEvent, style=style(
+ (StyleAttr.height, cstring"400px"), (StyleAttr.overflow, cstring"scroll"))):
+ for x in entries:
+ tdiv:
+ text x
+
+setRenderer createDom
diff --git a/experiments/style.css b/experiments/style.css
new file mode 100644
index 0000000..ceaf090
--- /dev/null
+++ b/experiments/style.css
@@ -0,0 +1,378 @@
+html,
+body {
+ margin: 0;
+ padding: 0;
+}
+
+.todomvc-wrapper {
+ visibility: visible !important;
+}
+
+button {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ background: none;
+ font-size: 100%;
+ vertical-align: baseline;
+ font-family: inherit;
+ font-weight: inherit;
+ color: inherit;
+ -webkit-appearance: none;
+ appearance: none;
+ -webkit-font-smoothing: antialiased;
+ -moz-font-smoothing: antialiased;
+ font-smoothing: antialiased;
+}
+
+body {
+ font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ line-height: 1.4em;
+ background: #f5f5f5;
+ color: #4d4d4d;
+ min-width: 230px;
+ max-width: 550px;
+ margin: 0 auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-font-smoothing: antialiased;
+ font-smoothing: antialiased;
+ font-weight: 300;
+}
+
+button,
+input[type="checkbox"] {
+ outline: none;
+}
+
+.hidden {
+ display: none;
+}
+
+.todoapp {
+ background: #fff;
+ margin: 130px 0 40px 0;
+ position: relative;
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
+ 0 25px 50px 0 rgba(0, 0, 0, 0.1);
+}
+
+.todoapp input::-webkit-input-placeholder {
+ font-style: italic;
+ font-weight: 300;
+ color: #e6e6e6;
+}
+
+.todoapp input::-moz-placeholder {
+ font-style: italic;
+ font-weight: 300;
+ color: #e6e6e6;
+}
+
+.todoapp input::input-placeholder {
+ font-style: italic;
+ font-weight: 300;
+ color: #e6e6e6;
+}
+
+.todoapp h1 {
+ position: absolute;
+ top: -155px;
+ width: 100%;
+ font-size: 100px;
+ font-weight: 100;
+ text-align: center;
+ color: rgba(175, 47, 47, 0.15);
+ -webkit-text-rendering: optimizeLegibility;
+ -moz-text-rendering: optimizeLegibility;
+ text-rendering: optimizeLegibility;
+}
+
+.new-todo,
+.edit {
+ position: relative;
+ margin: 0;
+ width: 100%;
+ font-size: 24px;
+ font-family: inherit;
+ font-weight: inherit;
+ line-height: 1.4em;
+ border: 0;
+ outline: none;
+ color: inherit;
+ padding: 6px;
+ border: 1px solid #999;
+ box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
+ box-sizing: border-box;
+ -webkit-font-smoothing: antialiased;
+ -moz-font-smoothing: antialiased;
+ font-smoothing: antialiased;
+}
+
+.new-todo {
+ padding: 16px 16px 16px 60px;
+ border: none;
+ background: rgba(0, 0, 0, 0.003);
+ box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
+}
+
+.main {
+ position: relative;
+ z-index: 2;
+ border-top: 1px solid #e6e6e6;
+}
+
+label[for='toggle-all'] {
+ display: none;
+}
+
+.toggle-all {
+ position: absolute;
+ top: -55px;
+ left: -12px;
+ width: 60px;
+ height: 34px;
+ text-align: center;
+ border: none; /* Mobile Safari */
+}
+
+.toggle-all:before {
+ content: '>';
+ font-size: 22px;
+ color: #e6e6e6;
+ padding: 10px 27px 10px 27px;
+}
+
+.toggle-all:checked:before {
+ color: #737373;
+}
+
+.todo-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.todo-list li {
+ position: relative;
+ font-size: 24px;
+ border-bottom: 1px solid #ededed;
+}
+
+.todo-list li:last-child {
+ border-bottom: none;
+}
+
+.todo-list li.editing {
+ border-bottom: none;
+ padding: 0;
+}
+
+.todo-list li.editing .edit {
+ display: block;
+ width: 506px;
+ padding: 13px 17px 12px 17px;
+ margin: 0 0 0 43px;
+}
+
+.todo-list li.editing .view {
+ display: none;
+}
+
+.todo-list li .toggle {
+ text-align: center;
+ width: 40px;
+ /* auto, since non-WebKit browsers doesn't support input styling */
+ height: auto;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ margin: auto 0;
+ border: none; /* Mobile Safari */
+ -webkit-appearance: none;
+ appearance: none;
+}
+
+.todo-list li .toggle:after {
+ content: url('data:image/svg+xml,');
+}
+
+.todo-list li .toggle:checked:after {
+ content: url('data:image/svg+xml,');
+}
+
+.todo-list li label {
+ white-space: pre-line;
+ word-break: break-all;
+ padding: 15px 60px 15px 15px;
+ margin-left: 45px;
+ display: block;
+ line-height: 1.2;
+ transition: color 0.4s;
+}
+
+.todo-list li.completed label {
+ color: #d9d9d9;
+ text-decoration: line-through;
+}
+
+.todo-list li .destroy {
+ display: none;
+ position: absolute;
+ top: 0;
+ right: 10px;
+ bottom: 0;
+ width: 40px;
+ height: 40px;
+ margin: auto 0;
+ font-size: 30px;
+ color: #cc9a9a;
+ margin-bottom: 11px;
+ transition: color 0.2s ease-out;
+}
+
+.todo-list li .destroy:hover {
+ color: #af5b5e;
+}
+
+.todo-list li .destroy:after {
+ content: 'x';
+}
+
+.todo-list li:hover .destroy {
+ display: block;
+}
+
+.todo-list li.editing:last-child {
+ margin-bottom: -1px;
+}
+
+.footer {
+ color: #777;
+ padding: 10px 15px;
+ height: 20px;
+ text-align: center;
+ border-top: 1px solid #e6e6e6;
+}
+
+.footer:before {
+ content: '';
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ height: 50px;
+ overflow: hidden;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
+ 0 8px 0 -3px #f6f6f6,
+ 0 9px 1px -3px rgba(0, 0, 0, 0.2),
+ 0 16px 0 -6px #f6f6f6,
+ 0 17px 2px -6px rgba(0, 0, 0, 0.2);
+}
+
+.todo-count {
+ float: left;
+ text-align: left;
+}
+
+.todo-count strong {
+ font-weight: 300;
+}
+
+.filters {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ position: absolute;
+ right: 0;
+ left: 0;
+}
+
+.filters li {
+ display: inline;
+}
+
+.filters li a {
+ color: inherit;
+ margin: 3px;
+ padding: 3px 7px;
+ text-decoration: none;
+ border: 1px solid transparent;
+ border-radius: 3px;
+}
+
+.filters li a.selected,
+.filters li a:hover {
+ border-color: rgba(175, 47, 47, 0.1);
+}
+
+.filters li a.selected {
+ border-color: rgba(175, 47, 47, 0.2);
+}
+
+.clear-completed,
+html .clear-completed:active {
+ float: right;
+ position: relative;
+ line-height: 20px;
+ text-decoration: none;
+ cursor: pointer;
+ position: relative;
+}
+
+.clear-completed:hover {
+ text-decoration: underline;
+}
+
+.info {
+ margin: 65px auto 0;
+ color: #bfbfbf;
+ /* font-size: 10px; */
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+ text-align: center;
+}
+
+.info p {
+ line-height: 1;
+}
+
+.info a {
+ color: inherit;
+ text-decoration: none;
+ font-weight: 400;
+}
+
+.info a:hover {
+ text-decoration: underline;
+}
+
+/*
+ Hack to remove background from Mobile Safari.
+ Can't use it globally since it destroys checkboxes in Firefox
+*/
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ .toggle-all,
+ .todo-list li .toggle {
+ background: none;
+ }
+
+ .todo-list li .toggle {
+ height: 40px;
+ }
+
+ .toggle-all {
+ -webkit-transform: rotate(90deg);
+ transform: rotate(90deg);
+ -webkit-appearance: none;
+ appearance: none;
+ }
+}
+
+@media (max-width: 430px) {
+ .footer {
+ height: 50px;
+ }
+
+ .filters {
+ bottom: 10px;
+ }
+}
\ No newline at end of file
diff --git a/experiments/trello/knete.nim b/experiments/trello/knete.nim
new file mode 100644
index 0000000..3a09ee3
--- /dev/null
+++ b/experiments/trello/knete.nim
@@ -0,0 +1,752 @@
+## Raw DOM manipulation. No DOM diff'ing, no cry.
+
+import macros, tables
+from strutils import startsWith, toLowerAscii, toUpperAscii
+
+when defined(js):
+ type kstring* = cstring
+else:
+ type kstring* = string
+
+type
+ Tag* {.pure.} = enum
+ text = "#text",
+
+ html, head, title, base, link, meta, style,
+ script, noscript,
+ body, section, nav, article, aside,
+ h1, h2, h3, h4, h5, h6,
+ header, footer, address, main
+
+ p, hr, pre, blockquote, ol, ul, li,
+ dl, dt, dd,
+ figure, figcaption,
+
+ tdiv = "div",
+
+ a, em, strong, small,
+ strikethrough = "s", cite, quote,
+ dfn, abbr, data, time, code, `var` = "var", samp,
+ kdb, sub, sup, italic = "i", bold = "b", underlined = "u",
+ mark, ruby, rt, rp, bdi, dbo, span, br, wbr,
+ ins, del, img, iframe, embed, `object` = "object",
+ param, video, audio, source, track, canvas, map,
+ area, svg, math,
+
+ table, caption, colgroup, col, tbody, thead,
+ tfoot, tr, td, th,
+
+ form, fieldset, legend, label, input, button,
+ select, datalist, optgroup, option, textarea,
+ keygen, output, progress, meter,
+ details, summary, command, menu
+
+type
+ EventKind* {.pure.} = enum ## The events supported by the virtual DOM.
+ onclick, ## An element is clicked.
+ oncontextmenu, ## An element is right-clicked.
+ ondblclick, ## An element is double clicked.
+ onkeyup, ## A key was released.
+ onkeydown, ## A key is pressed.
+ onkeypressed, # A key was pressed.
+ onfocus, ## An element got the focus.
+ onblur, ## An element lost the focus.
+ onchange, ## The selected value of an element was changed.
+ onscroll, ## The user scrolled within an element.
+
+ onmousedown, ## A pointing device button (usually a mouse) is pressed
+ ## on an element.
+ onmouseenter, ## A pointing device is moved onto the element that
+ ## has the listener attached.
+ onmouseleave, ## A pointing device is moved off the element that
+ ## has the listener attached.
+ onmousemove, ## A pointing device is moved over an element.
+ onmouseout, ## A pointing device is moved off the element that
+ ## has the listener attached or off one of its children.
+ onmouseover, ## A pointing device is moved onto the element that has
+ ## the listener attached or onto one of its children.
+ onmouseup, ## A pointing device button is released over an element.
+
+ ondrag, ## An element or text selection is being dragged (every 350ms).
+ ondragend, ## A drag operation is being ended (by releasing a mouse button
+ ## or hitting the escape key).
+ ondragenter, ## A dragged element or text selection enters a valid drop target.
+ ondragleave, ## A dragged element or text selection leaves a valid drop target.
+ ondragover, ## An element or text selection is being dragged over a valid
+ ## drop target (every 350ms).
+ ondragstart, ## The user starts dragging an element or text selection.
+ ondrop, ## An element is dropped on a valid drop target.
+
+ onsubmit, ## A form is submitted
+ oninput, ## An input value changes
+
+ onkeyupenter, ## vdom extension: an input field received the ENTER key press
+ onkeyuplater ## vdom extension: a key was pressed and some time
+ ## passed (useful for on-the-fly text completions)
+
+
+macro buildLookupTables(): untyped =
+ var a = newTree(nnkBracket)
+ for i in low(Tag)..high(Tag):
+ let x = $i
+ let y = if x[0] == '#': x else: toUpperAscii(x)
+ a.add(newCall("kstring", newLit(y)))
+ var e = newTree(nnkBracket)
+ for i in low(EventKind)..high(EventKind):
+ e.add(newCall("kstring", newLit(substr($i, 2))))
+
+ template tmpl(a, e) {.dirty.} =
+ const
+ toTagName*: array[Tag, kstring] = a
+ toEventName*: array[EventKind, kstring] = e
+
+ result = getAst tmpl(a, e)
+
+buildLookupTables()
+
+# ---------------- CSS ----------------------------------------------
+
+type
+ StyleAttr* {.pure.} = enum
+ ## The style attributes supported by the virtual DOM.
+ ## Reference: https://www.w3schools.com/jsref/dom_obj_style.asp
+ alignContent
+ alignItems
+ alignSelf
+ animation
+ animationDelay
+ animationDirection
+ animationDuration
+ animationFillMode
+ animationIterationCount
+ animationName
+ animationTimingFunction
+ animationPlayState
+ background
+ backgroundAttachment
+ backgroundColor
+ backgroundImage
+ backgroundPosition
+ backgroundRepeat
+ backgroundClip
+ backgroundOrigin
+ backgroundSize
+ backfaceVisibility
+ border
+ borderBottom
+ borderBottomColor
+ borderBottomLeftRadius
+ borderBottomRightRadius
+ borderBottomStyle
+ borderBottomWidth
+ borderCollapse
+ borderColor
+ borderImage
+ borderImageOutset
+ borderImageRepeat
+ borderImageSlice
+ borderImageSource
+ borderImageWidth
+ borderLeft
+ borderLeftColor
+ borderLeftStyle
+ borderLeftWidth
+ borderRadius
+ borderRight
+ borderRightColor
+ borderRightStyle
+ borderRightWidth
+ borderSpacing
+ borderStyle
+ borderTop
+ borderTopColor
+ borderTopLeftRadius
+ borderTopRightRadius
+ borderTopStyle
+ borderTopWidth
+ borderWidth
+ bottom
+ boxDecorationBreak
+ boxShadow
+ boxSizing
+ captionSide
+ clear
+ clip
+ color
+ columnCount
+ columnFill
+ columnGap
+ columnRule
+ columnRuleColor
+ columnRuleStyle
+ columnRuleWidth
+ columns
+ columnSpan
+ columnWidth
+ content
+ counterIncrement
+ counterReset
+ cursor
+ direction
+ display
+ emptyCells
+ filter
+ flex
+ flexBasis
+ flexDirection
+ flexFlow
+ flexGrow
+ flexShrink
+ flexWrap
+ cssFloat
+ font
+ fontFamily
+ fontSize
+ fontSizeAdjust
+ fontStretch
+ fontStyle
+ fontVariant
+ fontWeight
+ hangingPunctuation
+ height
+ hyphens
+ icon
+ imageOrientation
+ justifyContent
+ left
+ letterSpacing
+ lineHeight
+ listStyle
+ listStyleImage
+ listStylePosition
+ listStyleType
+ margin
+ marginBottom
+ marginLeft
+ marginRight
+ marginTop
+ maxHeight
+ maxWidth
+ minHeight
+ minWidth
+ navDown
+ navIndex
+ navLeft
+ navRight
+ navUp
+ opacity
+ order
+ orphans
+ outline
+ outlineColor
+ outlineOffset
+ outlineStyle
+ outlineWidth
+ overflow
+ overflowX
+ overflowY
+ padding
+ paddingBottom
+ paddingLeft
+ paddingRight
+ paddingTop
+ pageBreakAfter
+ pageBreakBefore
+ pageBreakInside
+ perspective
+ perspectiveOrigin
+ pointerEvents
+ position
+ quotes
+ resize
+ right
+ scrollbar3dLightColor # note: missing in w3schools reference
+ scrollbarArrowColor # note: missing in w3schools reference
+ scrollbarBaseColor # note: missing in w3schools reference
+ scrollbarDarkshadowColor # note: missing in w3schools reference
+ scrollbarFaceColor # note: missing in w3schools reference
+ scrollbarHighlightColor # note: missing in w3schools reference
+ scrollbarShadowColor # note: missing in w3schools reference
+ scrollbarTrackColor # note: missing in w3schools reference
+ tableLayout
+ tabSize
+ textAlign
+ textAlignLast
+ textDecoration
+ textDecorationColor
+ textDecorationLine
+ textDecorationStyle
+ textIndent
+ textJustify
+ textOverflow
+ textShadow
+ textTransform
+ top
+ transform
+ transformOrigin
+ transformStyle
+ transition
+ transitionDelay
+ transitionDuration
+ transitionProperty
+ transitionTimingFunction
+ unicodeBidi
+ userSelect
+ verticalAlign
+ visibility
+ whiteSpace
+ width
+ wordBreak
+ wordSpacing
+ wordWrap
+ widows
+ zIndex
+
+macro buildStyleLookupTable(): untyped =
+ var e = newTree(nnkBracket)
+ for i in low(StyleAttr)..high(StyleAttr):
+ e.add(newCall("kstring", newLit($i)))
+ template tmpl(e) {.dirty.} =
+ const
+ toStyleAttrName: array[StyleAttr, kstring] = e
+ result = getAst tmpl(e)
+
+buildStyleLookupTable()
+
+type
+ StyleObj {.importc.} = object
+ background*: cstring
+ backgroundAttachment*: cstring
+ backgroundColor*: cstring
+ backgroundImage*: cstring
+ backgroundPosition*: cstring
+ backgroundRepeat*: cstring
+ border*: cstring
+ borderBottom*: cstring
+ borderBottomColor*: cstring
+ borderBottomStyle*: cstring
+ borderBottomWidth*: cstring
+ borderColor*: cstring
+ borderLeft*: cstring
+ borderLeftColor*: cstring
+ borderLeftStyle*: cstring
+ borderLeftWidth*: cstring
+ borderRight*: cstring
+ borderRightColor*: cstring
+ borderRightStyle*: cstring
+ borderRightWidth*: cstring
+ borderStyle*: cstring
+ borderTop*: cstring
+ borderTopColor*: cstring
+ borderTopStyle*: cstring
+ borderTopWidth*: cstring
+ borderWidth*: cstring
+ bottom*: cstring
+ captionSide*: cstring
+ clear*: cstring
+ clip*: cstring
+ color*: cstring
+ cursor*: cstring
+ direction*: cstring
+ display*: cstring
+ emptyCells*: cstring
+ cssFloat*: cstring
+ font*: cstring
+ fontFamily*: cstring
+ fontSize*: cstring
+ fontStretch*: cstring
+ fontStyle*: cstring
+ fontVariant*: cstring
+ fontWeight*: cstring
+ height*: cstring
+ left*: cstring
+ letterSpacing*: cstring
+ lineHeight*: cstring
+ listStyle*: cstring
+ listStyleImage*: cstring
+ listStylePosition*: cstring
+ listStyleType*: cstring
+ margin*: cstring
+ marginBottom*: cstring
+ marginLeft*: cstring
+ marginRight*: cstring
+ marginTop*: cstring
+ maxHeight*: cstring
+ maxWidth*: cstring
+ minHeight*: cstring
+ minWidth*: cstring
+ overflow*: cstring
+ padding*: cstring
+ paddingBottom*: cstring
+ paddingLeft*: cstring
+ paddingRight*: cstring
+ paddingTop*: cstring
+ pageBreakAfter*: cstring
+ pageBreakBefore*: cstring
+ pointerEvents*: cstring
+ position*: cstring
+ right*: cstring
+ scrollbar3dLightColor*: cstring
+ scrollbarArrowColor*: cstring
+ scrollbarBaseColor*: cstring
+ scrollbarDarkshadowColor*: cstring
+ scrollbarFaceColor*: cstring
+ scrollbarHighlightColor*: cstring
+ scrollbarShadowColor*: cstring
+ scrollbarTrackColor*: cstring
+ tableLayout*: cstring
+ textAlign*: cstring
+ textDecoration*: cstring
+ textIndent*: cstring
+ textTransform*: cstring
+ transform*: cstring
+ top*: cstring
+ verticalAlign*: cstring
+ visibility*: cstring
+ width*: cstring
+ wordSpacing*: cstring
+ zIndex*: int
+ Style* = ref StyleObj
+
+ ElementObj {.importc.} = object
+ id*, class*, value*: cstring
+ disabled*: bool
+ style*: Style
+ Element* = ref ElementObj
+
+ DocumentObj {.importc.} = object
+ Document = ref DocumentObj
+
+proc setStyle(s: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.}
+
+proc applyStyles*(e: Element; pairs: openArray[(StyleAttr, kstring)]) =
+ for x in pairs:
+ e.style.setStyle(toStyleAttrName[x[0]], x[1])
+
+# ------- Tree manipulation ---------------------------
+
+var document {.importc.}: Document
+
+proc parent*(x: Element): Element {.importcpp: "#.parentNode".}
+
+proc up*(x: Element; className: cstring): Element =
+ result = x
+ while result != nil and result.class != className:
+ result = result.parent
+
+proc len*(x: Element): int {.importcpp: "#.childNodes.length".}
+proc `[]`*(x: Element; idx: int): Element {.importcpp: "#.childNodes[#]".}
+proc getElementById*(id: cstring): Element {.importc: "document.getElementById", nodecl.}
+proc add*(n, child: Element) {.importcpp: "appendChild".}
+
+proc removeChild(n, child: Element) {.importcpp.}
+proc replaceChild(n, newNode, oldNode: Element) {.importcpp.}
+
+proc replace*(self, by: Element) = replaceChild(self.parent, by, self)
+proc delete*(self: Element) = removeChild(self.parent, self)
+
+proc insertBefore(n, newNode, before: Element) {.importcpp.}
+proc insert*(before, newNode: Element) = insertBefore(before.parent, newNode, before)
+
+proc createTextNode(d: Document, text: cstring): Element {.importcpp.}
+proc text*(s: kstring): Element = createTextNode(document, s)
+
+iterator items*(n: Element): Element =
+ for i in 0.. 0:
+ result[L-1] = tcall2(result[L-1], tmpContext)
+ of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkTryStmt,
+ nnkFinally:
+ # recurse for every child:
+ result = copyNimNode(n)
+ for x in n:
+ result.add tcall2(x, tmpContext)
+ of nnkCaseStmt:
+ # recurse for children, but don't add call for case ident
+ result = copyNimNode(n)
+ result.add n[0]
+ for i in 1 ..< n.len:
+ result.add tcall2(n[i], tmpContext)
+ of nnkProcDef:
+ let name = getName n[0]
+ if name.startsWith"on":
+ # turn it into an anon proc:
+ let anon = copyNimTree(n)
+ anon[0] = newEmptyNode()
+ if tmpContext == nil:
+ error "no Element to attach the event handler to"
+ else:
+ result = newCall(evHandler(), tmpContext,
+ newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi"))
+ else:
+ result = n
+ of nnkVarSection, nnkLetSection, nnkConstSection:
+ result = n
+ of nnkCallKinds - {nnkInfix}:
+ let op = getName(n[0])
+ if isComponent(op) == ComponentKind.Tag:
+ let tmp = genSym(nskLet, "tmp")
+ let call = newCall(bindSym"tree", newDotExpr(bindSym"Tag", n[0]))
+ result = newTree(
+ if tmpContext == nil: nnkStmtListExpr else: nnkStmtList,
+ newLetStmt(tmp, call))
+ for i in 1 ..< n.len:
+ # named parameters are transformed into attributes or events:
+ let x = n[i]
+ if x.kind == nnkExprEqExpr:
+ let key = getName x[0]
+ if key.startsWith("on"):
+ result.add newCall(evHandler(),
+ tmp, newDotExpr(bindSym"EventKind", x[0]), x[1])
+ elif key in SpecialAttrs:
+ result.add newDotAsgn(tmp, key, x[1])
+ elif eqIdent(key, "style"):
+ result.add newCall(bindSym"applyStyles", tmp, toKstring x[1])
+ elif eqIdent(key, "setFocus"):
+ result.add newCall(bindSym"focus", tmp)
+ else:
+ result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
+ elif eqIdent(x, "setFocus"):
+ result.add newCall(bindSym"focus", tmp)
+ else:
+ result.add tcall2(x, tmp)
+ if tmpContext == nil:
+ result.add tmp
+ else:
+ result.add newCall(bindSym"add", tmpContext, tmp)
+ elif tmpContext != nil and op notin StmtContext:
+ var hasEventHandlers = false
+ for i in 1..= 0.16.1"
+requires "nim >= 0.18.0"
+requires "ws"
+requires "dotenv"
+skipDirs = @["examples", "experiments", "tests"]
-skipDirs = @["examples/todoapp"]
\ No newline at end of file
+bin = @["karax/tools/karun"]
+installExt = @["nim"]
diff --git a/karax/autocomplete.nim b/karax/autocomplete.nim
new file mode 100644
index 0000000..210ed4a
--- /dev/null
+++ b/karax/autocomplete.nim
@@ -0,0 +1,160 @@
+
+import karax, karaxdsl, vdom, kdom, jstrutils
+
+type
+ AutocompleteState* = ref object
+ choices, candidates: seq[cstring]
+ selected, maxMatches: int
+ showCandidates, controlPressed: bool
+
+proc newAutocomplete*(choices: seq[cstring]; maxMatches = 5): AutocompleteState =
+ ## Creates a new state for the autocomplete widget. ``maxMatches`` is the maximum
+ ## number of elements to show.
+ AutocompleteState(choices: choices, candidates: @[],
+ selected: -1, maxMatches: maxMatches, showCandidates: false,
+ controlPressed: false)
+
+proc autocomplete*(s: AutocompleteState; onselection: proc(s: cstring)): VNode =
+ var inp: VNode
+
+ proc commit(ev: Event) =
+ s.showCandidates = false
+ onselection(inp.dom.value)
+ when false:
+ if inp.dom != nil:
+ echo "setting to A ", inp.dom.value.isNil
+ result.text = inp.dom.value
+ else:
+ echo "setting to B ", inp.text.isNil
+ result.text = inp.text
+ for e in result.events:
+ if e[0] == EventKind.onchange:
+ e[1](ev, result)
+
+ proc onkeyuplater(ev: Event; n: VNode) =
+ if not s.controlPressed:
+ let v = n.value
+ s.candidates.setLen 0
+ for c in s.choices:
+ if v.len == 0 or c.containsIgnoreCase(v): s.candidates.add(c)
+
+ proc onkeydown(ev: Event; n: VNode) =
+ const
+ LEFT = 37
+ UP = 38
+ RIGHT = 39
+ DOWN = 40
+ TAB = 9
+ ESC = 27
+ ENTER = 13
+ # UP: Move focus to the previous item. If on first item, move focus to the input.
+ # If on the input, move focus to last item.
+ # DOWN: Move focus to the next item. If on last item, move focus to the input.
+ # If on the input, move focus to the first item.
+ # ESCAPE: Close the menu.
+ # ENTER: Select the currently focused item and close the menu.
+ # TAB: Select the currently focused item, close the menu, and
+ # move focus to the next focusable element
+ s.controlPressed = false
+ case ev.keyCode
+ of UP:
+ s.controlPressed = true
+ s.showCandidates = true
+ if s.selected > 0:
+ dec s.selected
+ n.setInputText s.candidates[s.selected]
+ of DOWN:
+ s.controlPressed = true
+ s.showCandidates = true
+ if s.selected < s.candidates.len - 1:
+ inc s.selected
+ n.setInputText s.candidates[s.selected]
+ of ESC:
+ s.showCandidates = false
+ s.controlPressed = true
+ of ENTER:
+ s.controlPressed = true
+# inp.setInputText s.choices[i]
+ commit(ev)
+ else:
+ discard
+
+ proc window(s: AutocompleteState): (int, int) =
+ var first, last: int
+ if s.selected >= 0:
+ first = s.selected - (s.maxMatches div 2)
+ last = s.selected + (s.maxMatches div 2)
+ if first < 0: first = 0
+ # too few because we had to trim first?
+ if (last - first + 1) < s.maxMatches: last = first + s.maxMatches - 1
+ else:
+ first = 0
+ last = s.maxMatches - 1
+ if last > high(s.candidates): last = high(s.candidates)
+ # still too few because we're at the end?
+ if (last - first + 1) < s.maxMatches:
+ first = last - s.maxMatches + 1
+ if first < 0: first = 0
+
+ result = (first, last)
+
+ inp = buildHtml:
+ input(onkeyuplater = onkeyuplater,
+ onkeydown = onkeydown,
+ # onblur = proc (ev: Event; n: VNode) = commit(ev),
+ onfocus = proc (ev: Event; n: VNode) =
+ onkeyuplater(ev, n)
+ s.showCandidates = true)
+
+ proc select(i: int): proc(ev: Event; n: VNode) =
+ result = proc(ev: Event; n: VNode) =
+ s.selected = i
+ s.showCandidates = false
+ inp.setInputText s.choices[i]
+ commit(ev)
+
+ result = buildHtml(table):
+ tr:
+ td:
+ inp
+ if s.showCandidates:
+ let (first, last) = window(s)
+ for i in first..last:
+ tr:
+ td(onclick = select(i), class = cstring"button " &
+ (if i == s.selected: cstring"is-primary" else: "")):
+ text s.candidates[i]
+
+when isMainModule:
+ const suggestions = @[cstring"ActionScript",
+ "AppleScript",
+ "Asp",
+ "BASIC",
+ "C",
+ "C++",
+ "Clojure",
+ "COBOL",
+ "Erlang",
+ "Fortran",
+ "Groovy",
+ "Haskell",
+ "Java",
+ "JavaScript",
+ "Lisp",
+ "Nim",
+ "Perl",
+ "PHP",
+ "Python",
+ "Ruby",
+ "Scala",
+ "Scheme"]
+
+ var s = newAutocomplete(suggestions)
+
+ proc main(): VNode =
+ result = buildHtml(tdiv):
+ autocomplete(s, proc (s: cstring) = echo "now ", s)#:
+ # proc onchange(ev: Event; n: VNode) =
+ # echo "now selected ", n.kind
+
+ setRenderer(main)
diff --git a/karax/compact.nim b/karax/compact.nim
new file mode 100644
index 0000000..4e5dd6d
--- /dev/null
+++ b/karax/compact.nim
@@ -0,0 +1,114 @@
+## Components in Karax are built by the ``.component`` macro annotation.
+
+when defined(js):
+ import jdict, kdom
+
+import macros, vdom, tables, strutils, kbase
+
+when defined(js):
+ var
+ vcomponents* = newJDict[cstring, proc(args: seq[VNode]): VNode]()
+else:
+ var
+ vcomponents* = newTable[kstring, proc(args: seq[VNode]): VNode]()
+
+type
+ ComponentKind* {.pure.} = enum
+ None,
+ Tag,
+ VNode
+
+var
+ allcomponents {.compileTime.} = initTable[string, ComponentKind]()
+
+proc isComponent*(x: string): ComponentKind {.compileTime.} =
+ allcomponents.getOrDefault(x)
+
+proc addTags() {.compileTime.} =
+ let x = (bindSym"VNodeKind").getTypeImpl
+ expectKind(x, nnkEnumTy)
+ for i in ord(VNodeKind.html)..ord(VNodeKind.high):
+ # +1 because of empty node at the start of the enum AST:
+ let tag = $x[i+1]
+ allcomponents[tag] = ComponentKind.Tag
+
+static:
+ addTags()
+
+proc unpack(symbolicType: NimNode; index: int): NimNode {.compileTime.} =
+ #let t = symbolicType.getTypeImpl
+ let t = repr(symbolicType)
+ case t
+ of "cstring":
+ result = quote do:
+ args[`index`].text
+ of "int", "VKey":
+ result = quote do:
+ args[`index`].intValue
+ of "bool":
+ result = quote do:
+ args[`index`].intValue != 0
+ elif t.endsWith"Kind":
+ result = quote do:
+ `symbolicType`(args[`index`].intValue)
+ else:
+ # just pass it along, maybe there is some conversion for it:
+ result = quote do:
+ args[`index`]
+
+proc newname*(n: NimNode): NimNode =
+ if n.kind == nnkPostfix:
+ n[1] = newname(n[1])
+ result = n
+ elif n.kind == nnkSym:
+ result = ident(n.strVal)
+ else:
+ result = n
+
+when defined(js):
+ macro compact*(prc: untyped): untyped =
+ ## A 'compact' tree generation proc is one that only depends on its
+ ## inputs and should be stored as a compact virtual DOM tree and
+ ## only expanded on demand (when its inputs changed).
+ var n = prc.copyNimNode
+ for i in 0..6: n.add prc[i].copyNimTree
+ expectKind(n, nnkProcDef)
+ if n[0].kind == nnkEmpty:
+ error("please pass a non anonymous proc", n[0])
+ let name = n[0]
+ let params = params(n)
+ let rettype = repr params[0]
+ var isvirtual = ComponentKind.None
+ if rettype == "VNode":
+ isvirtual = ComponentKind.VNode
+ else:
+ error "component must return VNode", params[0]
+ let realName = if name.kind == nnkPostfix: name[1] else: name
+ let nn = $realName
+ n[0] = ident("inner" & nn)
+ var unpackCall = newCall(n[0])
+ var counter = 0
+ for i in 1 ..< params.len:
+ let param = params[i]
+ let L = param.len
+ let typ = param[L-2]
+ for j in 0 .. L-3:
+ unpackCall.add unpack(typ, counter)
+ inc counter
+
+ template vwrapper(pname, unpackCall) {.dirty.} =
+ proc pname(args: seq[VNode]): VNode =
+ unpackCall
+
+ template vregister(key, val) =
+ bind jdict.`[]=`
+ `[]=`(vcomponents, kstring(key), val)
+
+ result = newTree(nnkStmtList, n)
+
+ if isvirtual == ComponentKind.VNode:
+ result.add getAst(vwrapper(newname name, unpackCall))
+ result.add getAst(vregister(newLit(nn), realName))
+ allcomponents[nn] = isvirtual
+ when defined(debugKaraxDsl):
+ echo repr result
diff --git a/karax/errors.nim b/karax/errors.nim
new file mode 100644
index 0000000..aedd103
--- /dev/null
+++ b/karax/errors.nim
@@ -0,0 +1,32 @@
+## Error handling logic for form input validation.
+
+import karax, jdict
+
+var
+ gerrorMsgs = newJDict[cstring, cstring]()
+ gerrorCounter = 0
+
+proc hasErrors*(): bool = gerrorCounter != 0
+
+proc hasError*(field: cstring): bool = gerrorMsgs.contains(field) and len(gerrorMsgs[field]) > 0
+
+proc disableOnError*(): cstring = toDisabled(hasErrors())
+
+proc getError*(field: cstring): cstring =
+ if not gerrorMsgs.contains(field):
+ result = ""
+ else:
+ result = gerrorMsgs[field]
+
+proc setError*(field, msg: cstring) =
+ let previous = getError(field)
+ if len(msg) == 0:
+ if len(previous) != 0: dec(gerrorCounter)
+ else:
+ if len(previous) == 0: inc(gerrorCounter)
+ gerrorMsgs[field] = msg
+
+proc clearErrors*() =
+ let m = gerrorMsgs
+ for k in m.keys:
+ setError(k, "")
diff --git a/karax/i18n.nim b/karax/i18n.nim
new file mode 100644
index 0000000..42274ff
--- /dev/null
+++ b/karax/i18n.nim
@@ -0,0 +1,152 @@
+## Karax -- Single page applications for Nim.
+
+## i18n support for Karax applications. We distinguish between:
+##
+## - translate: Lookup text translations
+## - localize: Localize Date and Time objects to local formats
+##
+## Localization has not yet been implemented.
+
+import languages, jdict, jstrutils
+
+var currentLanguage = Language.enUS
+
+proc getLanguage(): cstring =
+ {.emit: "`result` = (navigator.languages && navigator.languages.length) ? navigator.languages[0] : navigator.language;".}
+
+proc detectLanguage*(): Language =
+ let x = getLanguage()
+ for i in low(Language)..high(Language):
+ if languageToCode[i] == x: return i
+ return Language.enUS
+
+proc setCurrentLanguage*(newLanguage = detectLanguage()) =
+ currentLanguage = newLanguage
+
+proc getCurrentLanguage*(): Language = currentLanguage
+
+type Translation* = JDict[cstring, cstring]
+
+var
+ translations: array[Language, Translation]
+
+proc registerTranslation*(lang: Language; t: Translation) = translations[lang] = t
+
+proc addTranslation*(lang: Language; key, val: cstring) =
+ if translations[lang].isNil: translations[lang] = newJDict[cstring, cstring]()
+ translations[lang][key] = val
+
+proc translate(x: cstring): cstring =
+ let y = translations[currentLanguage]
+ if y != nil and y.contains(x):
+ result = y[x]
+ else:
+ result = x
+
+type TranslatedString* = distinct cstring
+
+template i18n*(x: string): TranslatedString = TranslatedString(translate(cstring x))
+
+proc raiseInvalidFormat(errmsg: string) =
+ raise newException(ValueError, errmsg)
+
+discard """
+ $[1:item|items]1$ selected.
+"""
+
+proc `{}`(x: cstring; i: int): char {.importcpp: "#.charCodeAt(#)".}
+proc sadd(s: JSeq[char]; c: char) {.importcpp: "#.push(String.fromCharCode(#))".}
+
+proc parseChoice(f: cstring; i, choice: int,
+ r: JSeq[char]) =
+ var i = i
+ while i < f.len:
+ var n = 0
+ let oldI = i
+ var toAdd = false
+ while i < f.len and f{i} >= '0' and f{i} <= '9':
+ n = n * 10 + ord(f{i}) - ord('0')
+ inc i
+ if oldI != i:
+ if f{i} == ':':
+ inc i
+ else:
+ raiseInvalidFormat"':' after number expected"
+ toAdd = choice == n
+ else:
+ # an else section does not start with a number:
+ toAdd = true
+ while i < f.len and f{i} != ']' and f{i} != '|':
+ if toAdd: r.sadd f{i}
+ inc i
+ if toAdd: break
+ inc i
+
+proc join(x: JSeq[char]): cstring {.importcpp: "#.join(\"\")".}
+proc add(x: JSeq[char]; y: cstring) =
+ for i in 0..= '0' and f{i} <= '9':
+ j = j * 10 + ord(f{i}) - ord('0')
+ inc i
+ let idx = if not negative: j-1 else: args.len-j
+ r.add args[idx]
+ of '$':
+ inc(i)
+ r.add '$'
+ of '[':
+ let start = i+1
+ while i < f.len and f{i} != ']': inc i
+ inc i
+ if i >= f.len: raiseInvalidFormat"']' expected"
+ case f{i}
+ of '#':
+ parseChoice(f, start, parseInt args[num], r)
+ inc i
+ inc num
+ of '1'..'9', '-':
+ var j = 0
+ var negative = f{i} == '-'
+ if negative: inc i
+ while f{i} >= '0' and f{i} <= '9':
+ j = j * 10 + ord(f{i}) - ord('0')
+ inc i
+ let idx = if not negative: j-1 else: args.len-j
+ parseChoice(f, start, parseInt args[idx], r)
+ else: raiseInvalidFormat"argument index expected after ']'"
+ else:
+ raiseInvalidFormat("'#', '$', or number expected")
+ if i < f.len and f{i} == '$': inc i
+ else:
+ r.sadd f{i}
+ inc i
+ result = join(r)
+
+
+when isMainModule:
+ addTranslation(Language.deDE, "$#$ -> $#$", "$2 macht $1")
+ setCurrentLanguage(Language.deDE)
+
+ echo(i18n"$#$ -> $#$" % [cstring"1", "2"])
+ echo(i18n"$[1:item |items ]1$ -> $1" % [cstring"1", "2"])
+ echo(i18n"$[1:item |items ]1 -> $1" % [cstring"0", "2"])
+ echo(i18n"$[1:item |items ]1 -> $1" % [cstring"2", "2"])
+ echo(i18n"$[1:item |items ]1$ -> $1" % [cstring"3", "2"])
+ echo(i18n"$1 $[1:item |4:huha |items ]1$ -> $1" % [cstring"4", "2"])
diff --git a/karax/jdict.nim b/karax/jdict.nim
new file mode 100644
index 0000000..c9e2224
--- /dev/null
+++ b/karax/jdict.nim
@@ -0,0 +1,30 @@
+
+type
+ JDict*[K, V] = ref object
+
+proc `[]`*[K, V](d: JDict[K, V], k: K): V {.importcpp: "#[#]".}
+proc `[]=`*[K, V](d: JDict[K, V], k: K, v: V) {.importcpp: "#[#] = #".}
+
+proc newJDict*[K, V](): JDict[K, V] {.importcpp: "{@}".}
+
+proc contains*[K, V](d: JDict[K, V], k: K): bool {.importcpp: "#.hasOwnProperty(#)".}
+
+proc del*[K, V](d: JDict[K, V], k: K) {.importcpp: "delete #[#]".}
+
+iterator keys*[K, V](d: JDict[K, V]): K =
+ var kkk: K
+ {.emit: ["for (", kkk, " in ", d, ") {"].}
+ yield kkk
+ {.emit: ["}"].}
+
+type
+ JSeq*[T] = ref object
+
+proc `[]`*[T](s: JSeq[T], i: int): T {.importcpp: "#[#]", noSideEffect.}
+proc `[]=`*[T](s: JSeq[T], i: int, v: T) {.importcpp: "#[#] = #", noSideEffect.}
+
+proc newJSeq*[T](len: int = 0): JSeq[T] {.importcpp: "new Array(#)".}
+proc len*[T](s: JSeq[T]): int {.importcpp: "#.length", noSideEffect.}
+proc add*[T](s: JSeq[T]; x: T) {.importcpp: "#.push(#)", noSideEffect.}
+
+proc shrink*[T](s: JSeq[T]; shorterLen: int) {.importcpp: "#.length = #", noSideEffect.}
diff --git a/karax/jjson.nim b/karax/jjson.nim
new file mode 100644
index 0000000..ad6106b
--- /dev/null
+++ b/karax/jjson.nim
@@ -0,0 +1,67 @@
+## This module implements some small zero-overhead 'JsonNode' type
+## and helper that maps directly to JavaScript objects.
+
+type
+ JsonNode* {.importc.} = ref object
+
+proc `[]`*(obj: JsonNode; fieldname: cstring): JsonNode {.importcpp: "#[#]".}
+proc `[]`*(obj: JsonNode; index: int): JsonNode {.importcpp: "#[#]".}
+proc `[]=`*[T](obj: JsonNode; fieldname: cstring; value: T)
+ {.importcpp: "#[#] = #".}
+proc length(x: JsonNode): int {.importcpp: "#.length".}
+proc len*(x: JsonNode): int = (if x.isNil: 0 else: x.length)
+
+proc newJsonNode*(fields: varargs[(cstring, JsonNode)]): JsonNode =
+ result = JsonNode()
+ for f in fields:
+ result[f[0]] = f[1]
+
+proc newJObject*(): JsonNode =
+ result = JsonNode()
+
+proc newJArray*(elements: varargs[JsonNode]): JsonNode {.importcpp: "#".}
+
+proc newJNull*(): JsonNode = nil
+
+template `%`*(x: typed): JsonNode = cast[JsonNode](x)
+template `%`*(x: string): JsonNode = cast[JsonNode](cstring x)
+
+proc getNum*(x: JsonNode): int {.importcpp: "#".}
+
+proc getInt*(x: JsonNode): int {.importcpp: "#".}
+proc getStr*(x: JsonNode): cstring {.importcpp: "#".}
+proc getFNum*(x: JsonNode): cstring {.importcpp: "#".}
+
+iterator items*(x: JsonNode): JsonNode =
+ for i in 0..=0)", nodecl.}
+
+proc containsIgnoreCase*(a, b: cstring): bool {.
+ importcpp: """(#.search(new RegExp(#.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$$\|]/g, "\\$$&") , "i"))>=0)""", nodecl.}
+
proc substr*(s: cstring; start: int): cstring {.importcpp: "substr", nodecl.}
proc substr*(s: cstring; start, length: int): cstring {.importcpp: "substr", nodecl.}
@@ -14,10 +20,14 @@ proc `&`*(a, b: cstring): cstring {.importcpp: "(# + #)", nodecl.}
proc toCstr*(s: int): cstring {.importcpp: "((#)+'')", nodecl.}
proc `&`*(s: int): cstring {.importcpp: "((#)+'')", nodecl.}
proc `&`*(s: bool): cstring {.importcpp: "((#)+'')", nodecl.}
+proc `&`*(s: float): cstring {.importcpp: "((#)+'')", nodecl.}
proc `&`*(s: cstring): cstring {.importcpp: "(#)", nodecl.}
proc isInt*(s: cstring): bool {.asmNoStackFrame.} =
asm """
- return s.match(/^[0-9]+$/);
+ return `s`.match(/^[0-9]+$/);
"""
+
+proc parseInt*(s: cstring): int {.importcpp: "parseInt(#, 10)", nodecl.}
+proc parseFloat*(s: cstring): BiggestFloat {.importc, nodecl.}
diff --git a/karax/jwebsockets.nim b/karax/jwebsockets.nim
new file mode 100644
index 0000000..7c94da7
--- /dev/null
+++ b/karax/jwebsockets.nim
@@ -0,0 +1,17 @@
+## Websockets support for the JSON backend.
+
+type
+ MessageEvent* {.importc.} = ref object
+ data*: cstring
+
+ WebSocket* {.importc.} = ref object
+ onmessage*: proc (e: MessageEvent)
+ onopen*: proc (e: MessageEvent)
+
+proc newWebSocket*(url, key: cstring): WebSocket
+ {.importcpp: "new WebSocket(@)".}
+
+proc newWebSocket*(url: cstring): WebSocket
+ {.importcpp: "new WebSocket(@)".}
+
+proc send*(w: WebSocket; data: cstring) {.importcpp.}
diff --git a/karax/kajax.nim b/karax/kajax.nim
new file mode 100644
index 0000000..4235b50
--- /dev/null
+++ b/karax/kajax.nim
@@ -0,0 +1,132 @@
+## Karax -- Single page applications for Nim.
+
+## This module implements support for `ajax`:idx: socket
+## handling.
+
+import karax
+import jsffi except `&`
+import jscore
+import dom
+
+type
+ ProgressEvent* {.importc.}= object
+ loaded*: float
+ total*: float
+
+ FormData* {.importc.} = JsObject
+
+ HttpRequest* {.importc.} = ref object
+ ThisObj {.importc.} = ref object
+ readyState, status: int
+ responseText, statusText: cstring
+
+ XMLHttpRequestUpload* {.importc.} = JsObject
+
+proc newFormData*(): FormData {.importcpp: "new FormData()".}
+proc append*(f: FormData, key: cstring, value: Blob) {.importcpp:"#.append(@)".}
+proc append*(f: FormData, key: cstring, value: cstring) {.importcpp:"#.append(@)".}
+
+proc setRequestHeader*(r: HttpRequest; a, b: cstring) {.importcpp: "#.setRequestHeader(@)".}
+proc statechange*(r: HttpRequest; cb: proc()) {.importcpp: "#.onreadystatechange = #".}
+proc send*(r: HttpRequest; data: cstring) {.importcpp: "#.send(#)".}
+proc send*(r: HttpRequest, data: Blob) {.importcpp: "#.send(#)".}
+proc open*(r: HttpRequest; meth, url: cstring; async: bool) {.importcpp: "#.open(@)".}
+proc newRequest*(): HttpRequest {.importcpp: "new XMLHttpRequest(@)".}
+
+when not declared(dom.File):
+ type
+ DomFile = ref FileObj
+ FileObj {.importc.} = object of Blob
+ lastModified: int
+ name: cstring
+else:
+ type
+ DomFile = dom.File
+
+proc uploadFile*(url: cstring, file: Blob, onprogress :proc(data: ProgressEvent),
+ cont: proc (httpStatus: int; response: cstring);
+ headers: openarray[(cstring, cstring)] = []) =
+ proc contWrapper(httpStatus: int; response: cstring) =
+ cont(httpStatus, response)
+
+ proc upload(r: HttpRequest):XMLHttpRequestUpload {.importcpp: "#.upload".}
+ var this {.importc: "this".}: ThisObj
+
+ var formData = newFormData()
+ formData.append("upload_file",file)
+ formData.append("filename", DomFile(file).name)
+ let ajax = newRequest()
+ ajax.open("POST", url, true)
+ for a, b in items(headers):
+ ajax.setRequestHeader(a, b)
+ ajax.statechange proc() =
+ if this.readyState == 4:
+ contWrapper(this.status, this.responseText)
+ ajax.upload.onprogress = onprogress
+ ajax.send(formData.to(cstring))
+
+proc ajax*(meth, url: cstring; headers: openarray[(cstring, cstring)];
+ data: cstring;
+ cont: proc (httpStatus: int; response: cstring);
+ doRedraw: bool = true,
+ kxi: KaraxInstance = kxi,
+ useBinary: bool = false,
+ blob: Blob = nil) =
+ proc contWrapper(httpStatus: int; response: cstring) =
+ cont(httpStatus, response)
+ if doRedraw: redraw(kxi)
+
+ var this {.importc: "this".}: ThisObj
+
+ let ajax = newRequest()
+ ajax.open(meth, url, true)
+ for a, b in items(headers):
+ ajax.setRequestHeader(a, b)
+ ajax.statechange proc() =
+ if this.readyState == 4:
+ if this.status == 200:
+ contWrapper(this.status, this.responseText)
+ else:
+ contWrapper(this.status, this.responseText)
+ if useBinary:
+ ajax.send(blob)
+ else:
+ ajax.send(data)
+
+proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)];
+ data: cstring;
+ cont: proc (httpStatus: int, response: cstring);
+ doRedraw: bool = true,
+ kxi: KaraxInstance = kxi) =
+ ajax("POST", url, headers, data, cont, doRedraw, kxi)
+
+proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)];
+ data: Blob;
+ cont: proc (httpStatus: int, response: cstring);
+ doRedraw: bool = true,
+ kxi: KaraxInstance = kxi) =
+ ajax("POST", url, headers, "", cont, doRedraw, kxi, true, data)
+
+proc ajaxGet*(url: cstring; headers: openarray[(cstring, cstring)];
+ cont: proc (httpStatus: int, response: cstring);
+ doRedraw: bool = true,
+ kxi: KaraxInstance = kxi) =
+ ajax("GET", url, headers, nil, cont, doRedraw, kxi)
+
+proc ajaxPut*(url: cstring; headers: openarray[(cstring, cstring)];
+ data: cstring;
+ cont: proc (httpStatus: int, response: cstring);
+ doRedraw: bool = true,
+ kxi: KaraxInstance = kxi) =
+ ajax("PUT", url, headers, data, cont, doRedraw, kxi)
+
+proc ajaxDelete*(url: cstring; headers: openarray[(cstring, cstring)];
+ cont: proc (httpStatus: int, response: cstring);
+ doRedraw: bool = true,
+ kxi: KaraxInstance = kxi) =
+ ajax("DELETE", url, headers, nil, cont, doRedraw, kxi)
+
+
+proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".}
+proc fromJson*[T](blob: cstring): T {.importc: "JSON.parse".}
+
diff --git a/karax/karax.nim b/karax/karax.nim
new file mode 100644
index 0000000..2246676
--- /dev/null
+++ b/karax/karax.nim
@@ -0,0 +1,834 @@
+## Karax -- Single page applications for Nim.
+
+import kdom, vdom, jstrutils, compact, jdict, vstyles
+
+export kdom.Event, kdom.Blob
+
+when defined(nimNoNil):
+ {.experimental: "notnil".}
+
+proc kout*[T](x: T) {.importc: "console.log", varargs.}
+ ## The preferred way of debugging karax applications.
+
+type
+ PatchKind = enum
+ pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach, pkSame
+ Patch = object
+ k: PatchKind
+ parent, current: Node
+ newNode, oldNode: VNode
+ PatchV = object
+ parent, newChild: VNode
+ pos: int
+ ComponentPair = object
+ oldNode, newNode: VComponent
+ parent, current: Node
+
+type
+ RouterData* = ref object ## information that is passed to the 'renderer' callback
+ hashPart*: cstring ## the hash part of the URL for routing.
+
+ KaraxInstance* = ref object ## underlying karax instance. Usually you don't have
+ ## know about this.
+ rootId: cstring
+ renderer: proc (data: RouterData): VNode {.closure.}
+ currentTree: VNode
+ postRenderCallback: proc (data: RouterData)
+ toFocus: Node
+ toFocusV: VNode
+ renderId: int
+ rendering: bool
+ patches: seq[Patch] # we reuse this to save allocations
+ patchLen: int
+ patchesV: seq[PatchV]
+ patchLenV: int
+ runCount: int
+ components: seq[ComponentPair]
+ surpressRedraws*: bool
+ byId: JDict[cstring, VNode]
+ when defined(stats):
+ recursion: int
+ orphans: JDict[cstring, bool]
+
+
+var
+ kxi*: KaraxInstance ## The current Karax instance. This is always used
+ ## as the default. **Note**: Within the karax DSL
+ ## always a symbol of the name *kxi* is assumed, so
+ ## if you have a local karax instance to use instead
+ ## in your 'buildHtml' statement, it needs to be named
+ ## 'kxi'.
+
+proc setFocus*(n: VNode; enabled = true; kxi: KaraxInstance = kxi) =
+ if enabled:
+ kxi.toFocusV = n
+
+# ----------------- event wrapping ---------------------------------------
+
+template nativeValue(ev): cstring = cast[Element](ev.target).value
+template setNativeValue(ev, val) = cast[Element](ev.target).value = val
+
+template keyeventBody() =
+ let v = nativeValue(ev)
+ n.value = v
+ assert action != nil
+ action(ev, n)
+ if n.value != v:
+ setNativeValue(ev, n.value)
+ # Do not call redraw() here! That is already done
+ # by ``karax.addEventHandler``.
+
+proc karaxEvents(d: Node): JSeq[(cstring, NativeEventHandler)] {.importcpp: "#.karaxEvents".}
+proc `karaxEvents=`(d: Node; value: JSeq[(cstring, NativeEventHandler)]) {.importcpp: "#.karaxEvents = #".}
+
+proc addEventShell(d: Node; name: cstring; h: NativeEventHandler) =
+ # The DOM is such a pathetic piece of junk that it doesn't
+ # offer 'removeAllEventHandlers()'. Hence we store the event
+ # handler twice in 'd' so that we can emulate this properly.
+ # This is required to fix bug #139.
+ d.addEventListener(name, h)
+ if d.karaxEvents == nil:
+ d.karaxEvents = newJSeq[(cstring, NativeEventHandler)]()
+ d.karaxEvents.add((name, h))
+
+proc removeAllEventHandlers(d: Node) =
+ if d.karaxEvents != nil:
+ for i in 0..= 0:
+ # result.key = n.key
+ for k, v in attrs(n):
+ if v != nil:
+ result.setAttr(k, v)
+ applyEvents(n)
+ if kxi != nil and n == kxi.toFocusV and kxi.toFocus.isNil:
+ kxi.toFocus = result
+ if not n.style.isNil: applyStyle(result, n.style)
+
+proc same(n: VNode, e: Node; nesting = 0): bool =
+ if kxi.orphans.contains(n.id): return true
+ if n.kind == VNodeKind.component:
+ result = same(VComponent(n).expanded, e, nesting+1)
+ elif n.kind == VNodeKind.verbatim:
+ result = true
+ elif n.kind == VNodeKind.vthunk or n.kind == VNodeKind.dthunk:
+ # we don't check these:
+ result = true
+ elif toTag[n.kind] == e.nodename:
+ result = true
+ if n.kind != VNodeKind.text:
+ # BUGFIX: Microsoft's Edge gives the textarea a child containing the text node!
+ if e.len != n.len and n.kind != VNodeKind.textarea:
+ echo "expected ", n.len, " real ", e.len, " ", toTag[n.kind], " nesting ", nesting
+ return false
+ for i in 0 ..< n.len:
+ if not same(n[i], e[i], nesting+1): return false
+ else:
+ echo "VDOM: ", toTag[n.kind], " DOM: ", e.nodename
+
+proc replaceById(id: cstring; newTree: Node) =
+ let x = document.getElementById(id)
+ x.parentNode.replaceChild(newTree, x)
+ newTree.id = id
+
+type
+ EqResult = enum
+ componentsIdentical, different, similar, identical, usenewNode
+
+when defined(profileKarax):
+ type
+ DifferEnum = enum
+ deKind, deId, deIndex, deText, deComponent, deClass,
+ deSimilar
+
+ var
+ reasons: array[DifferEnum, int]
+
+ proc echa(a: array[DifferEnum, int]) =
+ for i in low(DifferEnum)..high(DifferEnum):
+ echo i, " value: ", a[i]
+
+proc eq(a, b: VNode; recursive: bool): EqResult =
+ if a.kind != b.kind:
+ when defined(profileKarax): inc reasons[deKind]
+ return different
+ if a.id != b.id:
+ when defined(profileKarax): inc reasons[deId]
+ return different
+ result = identical
+ if a.index != b.index:
+ when defined(profileKarax): inc reasons[deIndex]
+ return different
+ if a.kind == VNodeKind.text:
+ if a.text != b.text:
+ when defined(profileKarax): inc reasons[deText]
+ return different # similar
+ elif a.kind == VNodeKind.vthunk:
+ if a.text != b.text: return different
+ if a.len != b.len: return different
+ for i in 0..>" & depth)
+ elif n.kind == VNodeKind.text:
+ kout depth, n.text
+ for i in 0 ..< n.len:
+ printV(n[i], depth & " ")
+
+proc addPatch(kxi: KaraxInstance; ka: PatchKind; parenta, currenta: Node;
+ na, oldNode: VNode) =
+ let L = kxi.patchLen
+ if L >= kxi.patches.len:
+ # allocate more space:
+ kxi.patches.add(Patch(k: ka, parent: parenta, current: currenta,
+ newNode: na, oldNode: oldNode))
+ else:
+ kxi.patches[L].k = ka
+ kxi.patches[L].parent = parenta
+ kxi.patches[L].current = currenta
+ kxi.patches[L].newNode = na
+ kxi.patches[L].oldNode = oldNode
+ inc kxi.patchLen
+
+proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) =
+ let L = kxi.patchLenV
+ if L >= kxi.patchesV.len:
+ # allocate more space:
+ kxi.patchesV.add(PatchV(parent: parent, newChild: newChild, pos: pos))
+ else:
+ kxi.patchesV[L].parent = parent
+ kxi.patchesV[L].newChild = newChild
+ kxi.patchesV[L].pos = pos
+ inc kxi.patchLenV
+
+proc moveDom(dest, src: VNode) =
+ dest.dom = src.dom
+ src.dom = nil
+ if dest.id != nil:
+ kxi.byId[dest.id] = dest
+ assert dest.len == src.len
+ for i in 0.. We cannot ever use the old VDOM, we have to use the new virtual
+# DOM. For identical nodes we need to take over the .dom field from the old
+# node since we don't recompute them. This must be done recursively. In
+# vnodeToDom we have to check whether the 'dom' field was already set. If so,
+# There is nothing to do.
+# "Similar" nodes can have the opposite effect; consider
+#
+# AAAABAAAA
+# AAAACDAAAA
+#
+# In this example B did change to C and 'D' is new. However, replacing B by
+# CD is fine.
+#
+
+
+proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance) =
+ when defined(stats):
+ if kxi.recursion > 100:
+ echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode, false)
+ if oldNode.kind == VNodeKind.text:
+ echo oldNode.text
+ inc kxi.recursion
+ let result = eq(newNode, oldNode, false)
+ case result
+ of componentsIdentical:
+ kxi.components.add ComponentPair(oldNode: VComponent(oldNode),
+ newNode: VComponent(newNode),
+ parent: parent,
+ current: current)
+ of identical, similar:
+ newNode.dom = oldNode.dom
+ if result == similar:
+ updateStyles(newNode, oldNode)
+ updateAttributes(newNode, oldNode)
+ if oldNode.kind == VNodeKind.text:
+ oldNode.text = newNode.text
+ oldNode.dom.nodeValue = newNode.text
+
+ if newNode.events.len != 0 or oldNode.events.len != 0:
+ mergeEvents(newNode, oldNode, kxi)
+
+ let newLength = newNode.len
+ let oldLength = oldNode.len
+ if newLength == 0 and oldLength == 0: return
+ let minLength = min(newLength, oldLength)
+
+ assert oldNode.kind == newNode.kind
+ var commonPrefix = 0
+
+ # compute common prefix:
+ while commonPrefix < minLength:
+ if eq(newNode[commonPrefix], oldNode[commonPrefix], true) == identical:
+ kxi.addPatch(pkSame, nil, nil, newNode[commonPrefix], oldNode[commonPrefix])
+ inc commonPrefix
+ else:
+ break
+
+ # compute common suffix:
+ var oldPos = oldLength - 1
+ var newPos = newLength - 1
+ while oldPos >= commonPrefix and newPos >= commonPrefix:
+ if eq(newNode[newPos], oldNode[oldPos], true) == identical:
+ kxi.addPatch(pkSame, nil, nil, newNode[newPos], oldNode[oldPos])
+ dec oldPos
+ dec newPos
+ else:
+ break
+
+ let pos = min(oldPos, newPos) + 1
+ # now the different children are in commonPrefix .. pos - 1:
+ for i in commonPrefix..pos-1:
+ diff(newNode[i], oldNode[i], current, current.childNodes[i], kxi)
+
+ if oldPos + 1 == oldLength:
+ for i in pos..newPos:
+ kxi.addPatch(pkAppend, current, nil, newNode[i], nil)
+ else:
+ let before = current.childNodes[oldPos + 1]
+ for i in pos..newPos:
+ kxi.addPatch(pkInsertBefore, current, before, newNode[i], nil)
+ # XXX call 'attach' here?
+ for i in pos..oldPos:
+ detach(oldNode[i])
+ #doAssert i < current.childNodes.len
+ kxi.addPatch(pkRemove, current, current.childNodes[i], nil, nil)
+ of different:
+ detach(oldNode)
+ kxi.addPatch(pkReplace, parent, current, newNode, nil)
+ of usenewNode: doAssert(false, "eq returned usenewNode")
+ when defined(stats):
+ dec kxi.recursion
+
+proc applyComponents(kxi: KaraxInstance) =
+ # the first 'diff' pass detects components in the VDOM. The
+ # 'applyComponents' expands components and so on until no
+ # components are left to check.
+ var i = 0
+ # beware: 'diff' appends to kxi.components!
+ # So this is actually a fixpoint iteration:
+ while i < kxi.components.len:
+ let x = kxi.components[i].oldNode
+ let newNode = kxi.components[i].newNode
+ when defined(karaxDebug):
+ echo "Processing component ", newNode.text, " changed impl set ", x.changedImpl != nil
+ if x.changedImpl != nil and x.changedImpl(x, newNode):
+ when defined(karaxDebug):
+ echo "Component ", newNode.text, " did change"
+ let current = kxi.components[i].current
+ let parent = kxi.components[i].parent
+ x.updatedImpl(x, newNode)
+ let oldExpanded = x.expanded
+ x.expanded = x.renderImpl(x)
+ when defined(karaxDebug):
+ echo "Component ", newNode.text, " re-rendered"
+ x.renderedVersion = x.version
+ if oldExpanded == nil:
+ detach(x)
+ kxi.addPatch(pkReplace, parent, current, x.expanded, nil)
+ when defined(karaxDebug):
+ echo "Component ", newNode.text, ": old expansion didn't exist"
+ else:
+ diff(x.expanded, oldExpanded, parent, current, kxi)
+ when false:
+ if res == usenewNode:
+ when defined(karaxDebug):
+ echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)"
+ discard "diff created a patchset for us, so this is fine"
+ elif res != different:
+ when defined(karaxDebug):
+ echo "Component ", newNode.text, ": re-render triggered no DOM change whatsoever"
+ x.expanded = oldExpanded
+ assert oldExpanded.dom != nil, "old expanded.dom is nil"
+ else:
+ when defined(karaxDebug):
+ echo "Component ", newNode.text, ": re-render triggered a DOM change (case B)"
+ assert x.expanded.dom != nil, "expanded.dom is nil"
+ inc i
+ setLen(kxi.components, 0)
+
+when defined(stats):
+ proc depth(n: VNode; total: var int): int =
+ var m = 0
+ for i in 0..= parent.len:
+ kxi.addPatch(pkAppend, current, nil, kid, nil)
+ parent.add(kid)
+ else:
+ let before = current.childNodes[position]
+ kxi.addPatch(pkInsertBefore, current, before, kid, nil)
+ parent.insert(kid, position)
+ applyPatch(kxi)
+ doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
+
+proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) =
+ let olddom = oldNode.dom
+ doAssert olddom != nil
+ diff(newNode, oldNode, nil, olddom, kxi)
+ # this is a bit nasty: Since we cannot patch the 'parent' of
+ # the current VNode (because we don't store it at all!), we
+ # need to override the fields individually:
+ takeOverFields(newNode, oldNode)
+ applyComponents(kxi)
+ applyPatch(kxi)
+ if kxi.currentTree == oldNode:
+ kxi.currentTree = newNode
+ doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
+
+var onhashChange {.importc: "window.onhashchange".}: proc()
+var hashPart {.importc: "window.location.hash".}: cstring
+
+proc avoidDomDiffing*(kxi: KaraxInstance = kxi) =
+ ## enforce a full redraw for the next redraw operation.
+ ## This can be used as a temporary way to workaround DOM diffing
+ ## problems or to avoid the DOM diffing when you already know
+ ## it should use a completely new DOM.
+ ## This is an experimental API.
+ kxi.currentTree = nil
+
+proc reqFrame(callback: proc()): int {.importc: "window.requestAnimationFrame".}
+when false:
+ proc cancelFrame(id: int) {.importc: "window.cancelAnimationFrame".}
+
+proc dodraw(kxi: KaraxInstance) =
+ if kxi.renderer.isNil: return
+ kxi.renderId = 0
+
+ if kxi.rendering:
+ # there is a render already running, delay 1 frame
+ kxi.renderId = reqFrame(proc () = kxi.dodraw)
+ return
+
+ kxi.rendering = true
+
+ let rdata = RouterData(hashPart: hashPart)
+ let newtree = kxi.renderer(rdata)
+ inc kxi.runCount
+ newtree.id = kxi.rootId
+ kxi.toFocus = nil
+ if kxi.currentTree == nil:
+ let asdom = toDom(newtree, useAttachedNode = true, kxi)
+ replaceById(kxi.rootId, asdom)
+ else:
+ doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
+ let olddom = document.getElementById(kxi.rootId)
+ diff(newtree, kxi.currentTree, nil, olddom, kxi)
+ when defined(profileKarax):
+ echo "<<<<<<<<<<<<<<"
+ echa reasons
+ applyComponents(kxi)
+ when defined(profileKarax):
+ echo "--------------"
+ echa reasons
+ echo ">>>>>>>>>>>>>>"
+ applyPatch(kxi)
+ kxi.currentTree = newtree
+ doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
+
+ if not kxi.postRenderCallback.isNil:
+ kxi.postRenderCallback(rdata)
+
+ # now that it's part of the DOM, give it the focus:
+ if kxi.toFocus != nil:
+ kxi.toFocus.focus()
+ kxi.rendering = false
+ when defined(stats):
+ kxi.recursion = 0
+ var total = 0
+ echo "depth ", depth(kxi.currentTree, total), " total ", total
+
+proc redraw*(kxi: KaraxInstance = kxi) =
+ # we buffer redraw requests:
+ when false:
+ if drawTimeout != nil:
+ clearTimeout(drawTimeout)
+ drawTimeout = setTimeout(dodraw, 30)
+ elif true:
+ if kxi.renderId == 0:
+ kxi.renderId = reqFrame(proc () = kxi.dodraw)
+ else:
+ dodraw(kxi)
+
+proc redrawSync*(kxi: KaraxInstance = kxi) = dodraw(kxi)
+
+proc init(ev: Event) =
+ kxi.renderId = reqFrame(proc () = kxi.dodraw)
+
+proc setRenderer*(renderer: proc (data: RouterData): VNode,
+ root: cstring = "ROOT",
+ clientPostRenderCallback:
+ proc (data: RouterData) = nil): KaraxInstance {.
+ discardable.} =
+ ## Setup Karax. Usually the return value can be ignored.
+ if document.getElementById(root).isNil:
+ let msg = "Could not find a
with id=" & root &
+ ". Karax needs it as its rendering target."
+ raise newException(Exception, $msg)
+
+ result = KaraxInstance(rootId: root, renderer: renderer,
+ postRenderCallback: clientPostRenderCallback,
+ patches: newSeq[Patch](60),
+ patchesV: newSeq[PatchV](30),
+ components: @[],
+ surpressRedraws: false,
+ byId: newJDict[cstring, VNode](),
+ orphans: newJDict[cstring, bool]())
+ kxi = result
+ window.onload = init
+ onhashChange = proc() = redraw()
+
+proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT",
+ clientPostRenderCallback: proc () = nil): KaraxInstance {.discardable.} =
+ ## Setup Karax. Usually the return value can be ignored.
+ proc wrapRenderer(data: RouterData): VNode = result = renderer()
+ proc wrapPostRender(data: RouterData) =
+ if clientPostRenderCallback != nil: clientPostRenderCallback()
+ setRenderer(wrapRenderer, root, wrapPostRender)
+
+proc setInitializer*(renderer: proc (data: RouterData): VNode, root: cstring = "ROOT",
+ clientPostRenderCallback:
+ proc (data: RouterData) = nil): KaraxInstance {.discardable.} =
+ ## Setup Karax. Usually the return value can be ignored.
+ result = KaraxInstance(rootId: root, renderer: renderer,
+ postRenderCallback: clientPostRenderCallback,
+ patches: newSeq[Patch](60),
+ patchesV: newSeq[PatchV](30),
+ components: @[],
+ surpressRedraws: true,
+ byId: newJDict[cstring, VNode](),
+ orphans: newJDict[cstring, bool]())
+ kxi = result
+ window.onload = init
+
+proc addEventHandler*(n: VNode; k: EventKind; action: EventHandler;
+ kxi: KaraxInstance = kxi) =
+ ## Implements the foundation of Karax's event management.
+ ## Karax DSL transforms ``tag(onEvent = handler)`` to
+ ## ``tempNode.addEventHandler(tagNode, EventKind.onEvent, wrapper)``
+ ## where ``wrapper`` calls the passed ``action`` and then triggers
+ ## a ``redraw``.
+ proc wrapper(ev: Event; n: VNode) =
+ action(ev, n)
+ if not kxi.surpressRedraws: redraw(kxi)
+ addEventListener(n, k, wrapper)
+
+proc addEventHandler*(n: VNode; k: EventKind; action: proc();
+ kxi: KaraxInstance = kxi) =
+ ## Implements the foundation of Karax's event management.
+ ## Karax DSL transforms ``tag(onEvent = handler)`` to
+ ## ``tempNode.addEventHandler(tagNode, EventKind.onEvent, wrapper)``
+ ## where ``wrapper`` calls the passed ``action`` and then triggers
+ ## a ``redraw``.
+ proc wrapper(ev: Event; n: VNode) =
+ action()
+ if not kxi.surpressRedraws: redraw(kxi)
+ addEventListener(n, k, wrapper)
+
+proc setOnHashChange*(action: proc (hashPart: cstring)) {.deprecated: "use setRenderer instead".} =
+ ## Now deprecated, instead pass a callback to ``setRenderer`` that receives
+ ## a ``data: RouterData`` parameter.
+ proc wrapper() =
+ action(hashPart)
+ redraw()
+ onhashchange = wrapper
+
+proc setForeignNodeId*(id: cstring; kxi: KaraxInstance = kxi) =
+ ## Declares a node ID as "foreign". Foreign nodes are not
+ ## under Karax's control in the sense that Karax does not attempt
+ ## to perform structural checks on them.
+ kxi.orphans[id] = true
+
+{.push stackTrace:off.}
+proc setupErrorHandler*() =
+ ## Installs an error handler that transforms native JS unhandled
+ ## exceptions into Nim based stack traces. If `useAlert` is false,
+ ## the error message is put into the console, otherwise `alert`
+ ## is called.
+ proc stackTraceAsCstring(): cstring = cstring(getStackTrace())
+ var onerror {.importc: "window.onerror", used.} =
+ proc (msg, url: cstring, line, col: int, error: cstring): bool =
+ var x = cstring"Error: " & msg & "\n" & stackTraceAsCstring()
+ echo(x)
+ return true # suppressErrorAlert
+{.pop.}
+
+proc prepend(parent, kid: Element) =
+ parent.insertBefore(kid, parent.firstChild)
+
+proc loadScript*(jsfilename: cstring; kxi: KaraxInstance = kxi) =
+ let s = document.createElement("script")
+ s.setAttr "type", "text/javascript"
+ s.setAttr "src", jsfilename
+ document.body.prepend(s)
+ redraw(kxi)
+
+proc runLater*(action: proc(); later = 400): Timeout {.discardable.} =
+ proc wrapper() =
+ action()
+ redraw()
+ result = setTimeout(wrapper, later)
+
+proc setInputText*(n: VNode; s: cstring) =
+ ## Sets the text of input elements.
+ n.text = s
+ if n.dom != nil: n.dom.value = s
+
+proc getInputText*(n: VNode): cstring =
+ if n.dom != nil:
+ result = n.dom.value
+
+proc toChecked*(checked: bool): cstring =
+ (if checked: cstring"checked" else: cstring(nil))
+
+proc toDisabled*(disabled: bool): cstring =
+ (if disabled: cstring"disabled" else: cstring(nil))
+
+proc toSelected*(selected: bool): cstring =
+ (if selected: cstring"selected" else: cstring(nil))
+
+proc vnodeToDom*(n: VNode; kxi: KaraxInstance = nil): Node =
+ result = toDom(n, useAttachedNode = false, kxi)
diff --git a/karax/karaxdsl.nim b/karax/karaxdsl.nim
new file mode 100644
index 0000000..451632c
--- /dev/null
+++ b/karax/karaxdsl.nim
@@ -0,0 +1,200 @@
+
+import macros, vdom, compact, kbase
+from strutils import startsWith, toLowerAscii
+
+when defined(js):
+ import karax
+
+const
+ StmtContext = ["kout", "inc", "echo", "dec", "!"]
+ SpecialAttrs = ["id", "class", "value", "index", "style"]
+
+proc getName(n: NimNode): string =
+ case n.kind
+ of nnkIdent, nnkSym:
+ result = $n
+ of nnkAccQuoted:
+ result = ""
+ for i in 0.. 0:
+ result[L-1] = tcall2(result[L-1], tmpContext)
+ of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkTryStmt,
+ nnkFinally:
+ # recurse for every child:
+ result = copyNimNode(n)
+ for x in n:
+ result.add tcall2(x, tmpContext)
+ of nnkCaseStmt:
+ # recurse for children, but don't add call for case ident
+ result = copyNimNode(n)
+ result.add n[0]
+ for i in 1 ..< n.len:
+ result.add tcall2(n[i], tmpContext)
+ of nnkProcDef:
+ let name = getName n[0]
+ if name.startsWith"on":
+ # turn it into an anon proc:
+ let anon = copyNimTree(n)
+ anon[0] = newEmptyNode()
+ if tmpContext == nil:
+ error "no VNode to attach the event handler to"
+ else:
+ result = newCall(evHandler(), tmpContext,
+ newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi"))
+ else:
+ result = n
+ of nnkVarSection, nnkLetSection, nnkConstSection:
+ result = n
+ of nnkCallKinds - {nnkInfix}:
+ let op = getName(n[0])
+ let ck = isComponent(op)
+ if ck != ComponentKind.None:
+ let tmp = genSym(nskLet, "tmp")
+ let call = if ck == ComponentKind.Tag:
+ newCall(bindSym"tree", newDotExpr(bindSym"VNodeKind", n[0]))
+ elif ck == ComponentKind.VNode:
+ newCall(bindSym"vthunk", newLit(op))
+ else:
+ newCall(bindSym"dthunk", newLit(op))
+ result = newTree(
+ if tmpContext == nil: nnkStmtListExpr else: nnkStmtList,
+ newLetStmt(tmp, call))
+ for i in 1 ..< n.len:
+ # named parameters are transformed into attributes or events:
+ let x = n[i]
+ if x.kind == nnkExprEqExpr:
+ let key = getName x[0]
+ if key.startsWith("on"):
+ result.add newCall(evHandler(),
+ tmp, newDotExpr(bindSym"EventKind", x[0]), x[1], ident("kxi"))
+ elif eqIdent(key, "style") and x[1].kind == nnkTableConstr:
+ result.add newDotAsgn(tmp, key, newCall("style", toKstring x[1]))
+ elif key in SpecialAttrs:
+ result.add newDotAsgn(tmp, key, x[1])
+ if key == "value":
+ result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
+ elif eqIdent(key, "setFocus"):
+ result.add newCall(key, tmp, x[1], ident"kxi")
+ elif eqIdent(key, "events"):
+ result.add newCall(bindSym"mergeEvents", tmp, x[1])
+ else:
+ result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
+ elif ck != ComponentKind.Tag:
+ call.add x
+ elif eqIdent(x, "setFocus"):
+ result.add newCall(x, tmp, bindSym"true", ident"kxi")
+ else:
+ result.add tcall2(x, tmp)
+ if tmpContext == nil:
+ result.add tmp
+ else:
+ result.add newCall(bindSym"add", tmpContext, tmp)
+ elif tmpContext != nil and op notin StmtContext:
+ var hasEventHandlers = false
+ for i in 1..`_.
+
+when not defined(js) and not defined(Nimdoc):
+ {.error: "This module only works on the JavaScript platform".}
+
+type
+ EventTarget* = ref EventTargetObj
+ EventTargetObj {.importc.} = object of RootObj
+ onabort*: proc (event: Event) {.nimcall.}
+ onblur*: proc (event: Event) {.nimcall.}
+ onchange*: proc (event: Event) {.nimcall.}
+ onclick*: proc (event: Event) {.nimcall.}
+ ondblclick*: proc (event: Event) {.nimcall.}
+ onerror*: proc (event: Event) {.nimcall.}
+ onfocus*: proc (event: Event) {.nimcall.}
+ onkeydown*: proc (event: Event) {.nimcall.}
+ onkeypress*: proc (event: Event) {.nimcall.}
+ onkeyup*: proc (event: Event) {.nimcall.}
+ onload*: proc (event: Event) {.nimcall.}
+ onmousedown*: proc (event: Event) {.nimcall.}
+ onmousemove*: proc (event: Event) {.nimcall.}
+ onmouseout*: proc (event: Event) {.nimcall.}
+ onmouseover*: proc (event: Event) {.nimcall.}
+ onmouseup*: proc (event: Event) {.nimcall.}
+ onreset*: proc (event: Event) {.nimcall.}
+ onselect*: proc (event: Event) {.nimcall.}
+ onsubmit*: proc (event: Event) {.nimcall.}
+ onunload*: proc (event: Event) {.nimcall.}
+
+ # https://developer.mozilla.org/en-US/docs/Web/Events
+ DomEvent* {.pure.} = enum
+ Abort = "abort",
+ BeforeInput = "beforeinput",
+ Blur = "blur",
+ Click = "click",
+ CompositionEnd = "compositionend",
+ CompositionStart = "compositionstart",
+ CompositionUpdate = "compositionupdate",
+ DblClick = "dblclick",
+ Error = "error",
+ Focus = "focus",
+ FocusIn = "focusin",
+ FocusOut = "focusout",
+ Input = "input",
+ KeyDown = "keydown",
+ KeyPress = "keypress",
+ KeyUp = "keyup",
+ Load = "load",
+ MouseDown = "mousedown",
+ MouseEnter = "mouseenter",
+ MouseLeave = "mouseleave",
+ MouseMove = "mousemove",
+ MouseOut = "mouseout",
+ MouseOver = "mouseover",
+ MouseUp = "mouseup",
+ Resize = "resize",
+ Scroll = "scroll",
+ Select = "select",
+ Unload = "unload",
+ Wheel = "wheel"
+
+ Window* = ref WindowObj
+ WindowObj {.importc.} = object of EventTargetObj
+ document*: Document
+ event*: Event
+ history*: History
+ location*: Location
+ closed*: bool
+ defaultStatus*: cstring
+ innerHeight*, innerWidth*: int
+ locationbar*: ref LocationBar
+ menubar*: ref MenuBar
+ name*: cstring
+ outerHeight*, outerWidth*: int
+ pageXOffset*, pageYOffset*: int
+ personalbar*: ref PersonalBar
+ scrollbars*: ref ScrollBars
+ statusbar*: ref StatusBar
+ status*: cstring
+ toolbar*: ref ToolBar
+ frames*: seq[Frame]
+ screen*: Screen
+
+ Frame* = ref FrameObj
+ FrameObj {.importc.} = object of WindowObj
+
+ ClassList* = ref ClassListObj
+ ClassListObj {.importc.} = object of RootObj
+
+ NodeType* = enum
+ ElementNode = 1,
+ AttributeNode,
+ TextNode,
+ CDATANode,
+ EntityRefNode,
+ EntityNode,
+ ProcessingInstructionNode,
+ CommentNode,
+ DocumentNode,
+ DocumentTypeNode,
+ DocumentFragmentNode,
+ NotationNode
+
+ Node* = ref NodeObj
+ NodeObj {.importc.} = object of EventTargetObj
+ attributes*: seq[Node]
+ childNodes*: seq[Node]
+ children*: seq[Node]
+ data*: cstring
+ firstChild*: Node
+ lastChild*: Node
+ nextSibling*: Node
+ nodeName*: cstring
+ nodeType*: NodeType
+ nodeValue*: cstring
+ parentNode*: Node
+ previousSibling*: Node
+ innerHTML*: cstring
+ style*: Style
+ when defined(nodejs):
+ id*: cstring
+
+ Document* = ref DocumentObj
+ DocumentObj {.importc.} = object of NodeObj
+ alinkColor*: cstring
+ bgColor*: cstring
+ body*: Element
+ charset*: cstring
+ cookie*: cstring
+ defaultCharset*: cstring
+ fgColor*: cstring
+ head*: Element
+ lastModified*: cstring
+ linkColor*: cstring
+ referrer*: cstring
+ title*: cstring
+ URL*: cstring
+ vlinkColor*: cstring
+ anchors*: seq[AnchorElement]
+ forms*: seq[FormElement]
+ images*: seq[ImageElement]
+ applets*: seq[Element]
+ embeds*: seq[EmbedElement]
+ links*: seq[LinkElement]
+
+ Element* = ref ElementObj
+ ElementObj {.importc.} = object of NodeObj
+ classList*: Classlist
+ checked*: bool
+ defaultChecked*: bool
+ defaultValue*: cstring
+ disabled*: bool
+ form*: FormElement
+ name*: cstring
+ readOnly*: bool
+ options*: seq[OptionElement]
+ selectedOptions*: seq[OptionElement]
+ clientWidth*, clientHeight*: int
+ contentEditable*: string
+ isContentEditable*: bool
+ dir*: string
+ offsetHeight*: int
+ offsetWidth*: int
+ offsetLeft*: int
+ offsetTop*: int
+
+ # https://developer.mozilla.org/en-US/docs/Web/API/ValidityState
+ ValidityState* = ref ValidityStateObj
+ ValidityStateObj {.importc.} = object
+ badInput*: bool
+ customError*: bool
+ patternMismatch*: bool
+ rangeOverflow*: bool
+ rangeUnderflow*: bool
+ stepMismatch*: bool
+ tooLong*: bool
+ tooShort*: bool
+ typeMismatch*: bool
+ valid*: bool
+ valueMissing*: bool
+
+ # https://developer.mozilla.org/en-US/docs/Web/API/Blob
+ Blob* = ref BlobObj
+ BlobObj {.importc.} = object of RootObj
+ size*: int
+ `type`*: cstring
+
+ # https://developer.mozilla.org/en-US/docs/Web/API/File
+ File* = ref FileObj
+ FileObj {.importc.} = object of Blob
+ lastModified*: int
+ name*: cstring
+
+ # https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement
+ InputElement* = ref InputElementObj
+ InputElementObj {.importc.} = object of Element
+ # Properties related to the parent form
+ formAction*: cstring
+ formEncType*: cstring
+ formMethod*: cstring
+ formNoValidate*: bool
+ formTarget*: cstring
+
+ # Properties that apply to any type of input element that is not hidden
+ `type`*: cstring
+ autofocus*: bool
+ required*: bool
+ value*: cstring
+ validity*: ValidityState
+ validationMessage*: cstring
+ willValidate*: bool
+
+ # Properties that apply only to elements of type "checkbox" or "radio"
+ indeterminate*: bool
+
+ # Properties that apply only to elements of type "image"
+ alt*: cstring
+ height*: cstring
+ src*: cstring
+ width*: cstring
+
+ # Properties that apply only to elements of type "file"
+ accept*: cstring
+ files*: seq[Blob]
+
+ # Properties that apply only to text/number-containing or elements
+ autocomplete*: cstring
+ maxLength*: int
+ size*: int
+ pattern*: cstring
+ placeholder*: cstring
+ min*: cstring
+ max*: cstring
+ selectionStart*: int
+ selectionEnd*: int
+ selectionDirection*: cstring
+
+ # Properties not yet categorized
+ dirName*: cstring
+ accessKey*: cstring
+ list*: Element
+ multiple*: bool
+ labels*: seq[Element]
+ step*: cstring
+ valueAsDate*: cstring
+ valueAsNumber*: float
+
+ LinkElement* = ref LinkObj
+ LinkObj {.importc.} = object of ElementObj
+ target*: cstring
+ text*: cstring
+ x*: int
+ y*: int
+
+ EmbedElement* = ref EmbedObj
+ EmbedObj {.importc.} = object of ElementObj
+ height*: int
+ hspace*: int
+ src*: cstring
+ width*: int
+ `type`*: cstring
+ vspace*: int
+
+ AnchorElement* = ref AnchorObj
+ AnchorObj {.importc.} = object of ElementObj
+ text*: cstring
+ x*, y*: int
+
+ OptionElement* = ref OptionObj
+ OptionObj {.importc.} = object of ElementObj
+ defaultSelected*: bool
+ selected*: bool
+ selectedIndex*: int
+ text*: cstring
+ value*: cstring
+
+ # https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement
+ FormElement* = ref FormObj
+ FormObj {.importc.} = object of ElementObj
+ acceptCharset*: cstring
+ action*: cstring
+ autocomplete*: cstring
+ elements*: seq[Element]
+ encoding*: cstring
+ enctype*: cstring
+ length*: int
+ `method`*: cstring
+ noValidate*: bool
+ target*: cstring
+
+ ImageElement* = ref ImageObj
+ ImageObj {.importc.} = object of ElementObj
+ border*: int
+ complete*: bool
+ height*: int
+ hspace*: int
+ lowsrc*: cstring
+ src*: cstring
+ vspace*: int
+ width*: int
+
+ Style* = ref StyleObj
+ StyleObj {.importc.} = object of RootObj
+ background*: cstring
+ backgroundAttachment*: cstring
+ backgroundColor*: cstring
+ backgroundImage*: cstring
+ backgroundPosition*: cstring
+ backgroundRepeat*: cstring
+ border*: cstring
+ borderBottom*: cstring
+ borderBottomColor*: cstring
+ borderBottomStyle*: cstring
+ borderBottomWidth*: cstring
+ borderColor*: cstring
+ borderLeft*: cstring
+ borderLeftColor*: cstring
+ borderLeftStyle*: cstring
+ borderLeftWidth*: cstring
+ borderRight*: cstring
+ borderRightColor*: cstring
+ borderRightStyle*: cstring
+ borderRightWidth*: cstring
+ borderStyle*: cstring
+ borderTop*: cstring
+ borderTopColor*: cstring
+ borderTopStyle*: cstring
+ borderTopWidth*: cstring
+ borderWidth*: cstring
+ bottom*: cstring
+ captionSide*: cstring
+ clear*: cstring
+ clip*: cstring
+ color*: cstring
+ cursor*: cstring
+ direction*: cstring
+ display*: cstring
+ emptyCells*: cstring
+ cssFloat*: cstring
+ font*: cstring
+ fontFamily*: cstring
+ fontSize*: cstring
+ fontStretch*: cstring
+ fontStyle*: cstring
+ fontVariant*: cstring
+ fontWeight*: cstring
+ height*: cstring
+ left*: cstring
+ letterSpacing*: cstring
+ lineHeight*: cstring
+ listStyle*: cstring
+ listStyleImage*: cstring
+ listStylePosition*: cstring
+ listStyleType*: cstring
+ margin*: cstring
+ marginBottom*: cstring
+ marginLeft*: cstring
+ marginRight*: cstring
+ marginTop*: cstring
+ maxHeight*: cstring
+ maxWidth*: cstring
+ minHeight*: cstring
+ minWidth*: cstring
+ overflow*: cstring
+ padding*: cstring
+ paddingBottom*: cstring
+ paddingLeft*: cstring
+ paddingRight*: cstring
+ paddingTop*: cstring
+ pageBreakAfter*: cstring
+ pageBreakBefore*: cstring
+ pointerEvents*: cstring
+ position*: cstring
+ right*: cstring
+ scrollbar3dLightColor*: cstring
+ scrollbarArrowColor*: cstring
+ scrollbarBaseColor*: cstring
+ scrollbarDarkshadowColor*: cstring
+ scrollbarFaceColor*: cstring
+ scrollbarHighlightColor*: cstring
+ scrollbarShadowColor*: cstring
+ scrollbarTrackColor*: cstring
+ tableLayout*: cstring
+ textAlign*: cstring
+ textDecoration*: cstring
+ textIndent*: cstring
+ textTransform*: cstring
+ transform*: cstring
+ top*: cstring
+ verticalAlign*: cstring
+ visibility*: cstring
+ width*: cstring
+ wordSpacing*: cstring
+ zIndex*: int
+
+ EventPhase* = enum
+ None = 0,
+ CapturingPhase,
+ AtTarget,
+ BubblingPhase
+
+ # https://developer.mozilla.org/en-US/docs/Web/API/Event
+ Event* = ref EventObj
+ EventObj {.importc.} = object of RootObj
+ bubbles*: bool
+ cancelBubble*: bool
+ cancelable*: bool
+ composed*: bool
+ currentTarget*: Node
+ defaultPrevented*: bool
+ eventPhase*: int
+ target*: Node
+ `type`*: cstring
+ isTrusted*: bool
+
+ # https://developer.mozilla.org/en-US/docs/Web/API/UIEvent
+ UIEvent* = ref UIEventObj
+ UIEventObj {.importc.} = object of Event
+ detail*: int64
+ view*: Window
+
+ # https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
+ KeyboardEvent* = ref KeyboardEventObj
+ KeyboardEventObj {.importc.} = object of UIEvent
+ altKey*, ctrlKey*, metaKey*, shiftKey*: bool
+ code*: cstring
+ isComposing*: bool
+ key*: cstring
+ keyCode*: int
+ location*: int
+
+ # https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
+ KeyboardEventKey* {.pure.} = enum
+ # Modifier keys
+ Alt,
+ AltGraph,
+ CapsLock,
+ Control,
+ Fn,
+ FnLock,
+ Hyper,
+ Meta,
+ NumLock,
+ ScrollLock,
+ Shift,
+ Super,
+ Symbol,
+ SymbolLock,
+
+ # Whitespace keys
+ ArrowDown,
+ ArrowLeft,
+ ArrowRight,
+ ArrowUp,
+ End,
+ Home,
+ PageDown,
+ PageUp,
+
+ # Editing keys
+ Backspace,
+ Clear,
+ Copy,
+ CrSel,
+ Cut,
+ Delete,
+ EraseEof,
+ ExSel,
+ Insert,
+ Paste,
+ Redo,
+ Undo,
+
+ # UI keys
+ Accept,
+ Again,
+ Attn,
+ Cancel,
+ ContextMenu,
+ Escape,
+ Execute,
+ Find,
+ Finish,
+ Help,
+ Pause,
+ Play,
+ Props,
+ Select,
+ ZoomIn,
+ ZoomOut,
+
+ # Device keys
+ BrigtnessDown,
+ BrigtnessUp,
+ Eject,
+ LogOff,
+ Power,
+ PowerOff,
+ PrintScreen,
+ Hibernate,
+ Standby,
+ WakeUp,
+
+ # Common IME keys
+ AllCandidates,
+ Alphanumeric,
+ CodeInput,
+ Compose,
+ Convert,
+ Dead,
+ FinalMode,
+ GroupFirst,
+ GroupLast,
+ GroupNext,
+ GroupPrevious,
+ ModeChange,
+ NextCandidate,
+ NonConvert,
+ PreviousCandidate,
+ Process,
+ SingleCandidate,
+
+ # Korean keyboards only
+ HangulMode,
+ HanjaMode,
+ JunjaMode,
+
+ # Japanese keyboards only
+ Eisu,
+ Hankaku,
+ Hiragana,
+ HiraganaKatakana,
+ KanaMode,
+ KanjiMode,
+ Katakana,
+ Romaji,
+ Zenkaku,
+ ZenkakuHanaku,
+
+ # Function keys
+ F1,
+ F2,
+ F3,
+ F4,
+ F5,
+ F6,
+ F7,
+ F8,
+ F9,
+ F10,
+ F11,
+ F12,
+ F13,
+ F14,
+ F15,
+ F16,
+ F17,
+ F18,
+ F19,
+ F20,
+ Soft1,
+ Soft2,
+ Soft3,
+ Soft4,
+
+ # Phone keys
+ AppSwitch,
+ Call,
+ Camera,
+ CameraFocus,
+ EndCall,
+ GoBack,
+ GoHome,
+ HeadsetHook,
+ LastNumberRedial,
+ Notification,
+ MannerMode,
+ VoiceDial,
+
+ # Multimedia keys
+ ChannelDown,
+ ChannelUp,
+ MediaFastForward,
+ MediaPause,
+ MediaPlay,
+ MediaPlayPause,
+ MediaRecord,
+ MediaRewind,
+ MediaStop,
+ MediaTrackNext,
+ MediaTrackPrevious,
+
+ # Audio control keys
+ AudioBalanceLeft,
+ AudioBalanceRight,
+ AudioBassDown,
+ AudioBassBoostDown,
+ AudioBassBoostToggle,
+ AudioBassBoostUp,
+ AudioBassUp,
+ AudioFaderFront,
+ AudioFaderRear,
+ AudioSurroundModeNext,
+ AudioTrebleDown,
+ AudioTrebleUp,
+ AudioVolumeDown,
+ AUdioVolumeMute,
+ AudioVolumeUp,
+ MicrophoneToggle,
+ MicrophoneVolumeDown,
+ MicrophoneVolumeMute,
+ MicrophoneVolumeUp,
+
+ # TV control keys
+ TV,
+ TV3DMode,
+ TVAntennaCable,
+ TVAudioDescription,
+ TVAudioDescriptionMixDown,
+ TVAudioDescriptionMixUp,
+ TVContentsMenu,
+ TVDataService,
+ TVInput,
+ TVInputComponent1,
+ TVInputComponent2,
+ TVInputComposite1,
+ TVInputComposite2,
+ TVInputHDMI1,
+ TVInputHDMI2,
+ TVInputHDMI3,
+ TVInputHDMI4,
+ TVInputVGA1,
+ TVMediaContext,
+ TVNetwork,
+ TVNumberEntry,
+ TVPower,
+ TVRadioService,
+ TVSatellite,
+ TVSatelliteBS,
+ TVSatelliteCS,
+ TVSatelliteToggle,
+ TVTerrestrialAnalog,
+ TVTerrestrialDigital,
+ TVTimer,
+
+ # Media controller keys
+ AVRInput,
+ AVRPower,
+ ColorF0Red,
+ ColorF1Green,
+ ColorF2Yellow,
+ ColorF3Blue,
+ ColorF4Grey,
+ ColorF5Brown,
+ ClosedCaptionToggle,
+ Dimmer,
+ DisplaySwap,
+ DVR,
+ Exit,
+ FavoriteClear0,
+ FavoriteClear1,
+ FavoriteClear2,
+ FavoriteClear3,
+ FavoriteRecall0,
+ FavoriteRecall1,
+ FavoriteRecall2,
+ FavoriteRecall3,
+ FavoriteStore0,
+ FavoriteStore1,
+ FavoriteStore2,
+ FavoriteStore3,
+ Guide,
+ GuideNextDay,
+ GuidePreviousDay,
+ Info,
+ InstantReplay,
+ Link,
+ ListProgram,
+ LiveContent,
+ Lock,
+ MediaApps,
+ MediaAudioTrack,
+ MediaLast,
+ MediaSkipBackward,
+ MediaSkipForward,
+ MediaStepBackward,
+ MediaStepForward,
+ MediaTopMenu,
+ NavigateIn,
+ NavigateNext,
+ NavigateOut,
+ NavigatePrevious,
+ NextFavoriteChannel,
+ NextUserProfile,
+ OnDemand,
+ Pairing,
+ PinPDown,
+ PinPMove,
+ PinPUp,
+ PlaySpeedDown,
+ PlaySpeedReset,
+ PlaySpeedUp,
+ RandomToggle,
+ RcLowBattery,
+ RecordSpeedNext,
+ RfBypass,
+ ScanChannelsToggle,
+ ScreenModeNext,
+ Settings,
+ SplitScreenToggle,
+ STBInput,
+ STBPower,
+ Subtitle,
+ Teletext,
+ VideoModeNext,
+ Wink,
+ ZoomToggle,
+
+ # Speech recognition keys
+ SpeechCorrectionList,
+ SpeechInputToggle,
+
+ # Document keys
+ Close,
+ New,
+ Open,
+ Print,
+ Save,
+ SpellCheck,
+ MailForward,
+ MailReply,
+ MailSend,
+
+ # Application selector keys
+ LaunchCalculator,
+ LaunchCalendar,
+ LaunchContacts,
+ LaunchMail,
+ LaunchMediaPlayer,
+ LaunchMusicPlayer,
+ LaunchMyComputer,
+ LaunchPhone,
+ LaunchScreenSaver,
+ LaunchSpreadsheet,
+ LaunchWebBrowser,
+ LaunchWebCam,
+ LaunchWordProcessor,
+ LaunchApplication1,
+ LaunchApplication2,
+ LaunchApplication3,
+ LaunchApplication4,
+ LaunchApplication5,
+ LaunchApplication6,
+ LaunchApplication7,
+ LaunchApplication8,
+ LaunchApplication9,
+ LaunchApplication10,
+ LaunchApplication11,
+ LaunchApplication12,
+ LaunchApplication13,
+ LaunchApplication14,
+ LaunchApplication15,
+ LaunchApplication16,
+
+ # Browser control keys
+ BrowserBack,
+ BrowserFavorites,
+ BrowserForward,
+ BrowserHome,
+ BrowserRefresh,
+ BrowserSearch,
+ BrowserStop,
+
+ # Numeric keypad keys
+ Key11,
+ Key12,
+ Separator
+
+ MouseButtons* = enum
+ NoButton = 0,
+ PrimaryButton = 1,
+ SecondaryButton = 2,
+ AuxilaryButton = 4,
+ FourthButton = 8,
+ FifthButton = 16
+
+ # https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent
+ MouseEvent* = ref MouseEventObj
+ MouseEventObj {.importc.} = object of UIEvent
+ altKey*, ctrlKey*, metaKey*, shiftKey*: bool
+ button*: int
+ buttons*: int
+ clientX*, clientY*: int
+ movementX*, movementY*: int
+ offsetX*, offsetY*: int
+ pageX*, pageY*: int
+ relatedTarget*: EventTarget
+ #region*: cstring
+ screenX*, screenY*: int
+ x*, y*: int
+
+ DataTransferItemKind* {.pure.} = enum
+ File = "file",
+ String = "string"
+
+ # https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem
+ DataTransferItem* = ref DataTransferItemObj
+ DataTransferItemObj {.importc.} = object of RootObj
+ kind*: cstring
+ `type`*: cstring
+
+ # https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer
+ DataTransfer* = ref DataTransferObj
+ DataTransferObj {.importc.} = object of RootObj
+ dropEffect*: cstring
+ effectAllowed*: cstring
+ files*: seq[Element]
+ items*: seq[DataTransferItem]
+ types*: seq[cstring]
+
+ DataTransferDropEffect* {.pure.} = enum
+ None = "none",
+ Copy = "copy",
+ Link = "link",
+ Move = "move"
+
+ DataTransferEffectAllowed* {.pure.} = enum
+ None = "none",
+ Copy = "copy",
+ CopyLink = "copyLink",
+ CopyMove = "copyMove",
+ Link = "link",
+ LinkMove = "linkMove",
+ Move = "move",
+ All = "all",
+ Uninitialized = "uninitialized"
+
+ DragEventTypes* = enum
+ Drag = "drag",
+ DragEnd = "dragend",
+ DragEnter = "dragenter",
+ DragExit = "dragexit",
+ DragLeave = "dragleave",
+ DragOver = "dragover",
+ DragStart = "dragstart",
+ Drop = "drop"
+
+ # https://developer.mozilla.org/en-US/docs/Web/API/DragEvent
+ DragEvent* {.importc.} = object of MouseEvent
+ dataTransfer*: DataTransfer
+
+ TouchList* {.importc.} = ref object of RootObj
+ length*: int
+
+ Touch* = ref TouchObj
+ TouchObj {.importc.} = object of RootObj
+ identifier*: int
+ screenX*, screenY*, clientX*, clientY*, pageX*, pageY*: int
+ target*: Element
+ radiusX*, radiusY*: int
+ rotationAngle*: int
+ force*: float
+
+ TouchEvent* = ref TouchEventObj
+ TouchEventObj {.importc.} = object of UIEvent
+ changedTouches*, targetTouches*, touches*: seq[Touch]
+
+ Location* = ref LocationObj
+ LocationObj {.importc.} = object of RootObj
+ hash*: cstring
+ host*: cstring
+ hostname*: cstring
+ href*: cstring
+ pathname*: cstring
+ port*: cstring
+ protocol*: cstring
+ search*: cstring
+
+ History* = ref HistoryObj
+ HistoryObj {.importc.} = object of RootObj
+ length*: int
+
+ Navigator* = ref NavigatorObj
+ NavigatorObj {.importc.} = object of RootObj
+ appCodeName*: cstring
+ appName*: cstring
+ appVersion*: cstring
+ cookieEnabled*: bool
+ language*: cstring
+ platform*: cstring
+ userAgent*: cstring
+ mimeTypes*: seq[ref MimeType]
+
+ Plugin* {.importc.} = object of RootObj
+ description*: cstring
+ filename*: cstring
+ name*: cstring
+
+ MimeType* {.importc.} = object of RootObj
+ description*: cstring
+ enabledPlugin*: ref Plugin
+ suffixes*: seq[cstring]
+ `type`*: cstring
+
+ LocationBar* {.importc.} = object of RootObj
+ visible*: bool
+ MenuBar* = LocationBar
+ PersonalBar* = LocationBar
+ ScrollBars* = LocationBar
+ ToolBar* = LocationBar
+ StatusBar* = LocationBar
+
+ Screen = ref ScreenObj
+ ScreenObj {.importc.} = object of RootObj
+ availHeight*: int
+ availWidth*: int
+ colorDepth*: int
+ height*: int
+ pixelDepth*: int
+ width*: int
+
+ TimeOut* {.importc.} = ref object of RootObj
+ Interval* {.importc.} = object of RootObj
+
+ AddEventListenerOptions* = object
+ capture*: bool
+ once*: bool
+ passive*: bool
+
+when defined(nodejs):
+ # we provide a dummy DOM for nodejs for testing purposes
+ proc len*(x: Node): int = x.childNodes.len
+ proc `[]`*(x: Node; idx: int): Element =
+ assert idx >= 0 and idx < x.childNodes.len
+ result = cast[Element](x.childNodes[idx])
+
+ var document* = Document(nodeType: DocumentNode)
+
+ proc getElem(x: Element; id: cstring): Element =
+ if x.id == id: return x
+ for i in 0..= 0 and rect.left >= 0 and
+ rect.bottom <= clientHeight().float and
+ rect.right <= clientWidth().float
+
+proc scrollTop*(e: Node): int {.importcpp: "#.scrollTop", nodecl.}
+proc offsetHeight*(e: Node): int {.importcpp: "#.offsetHeight", nodecl.}
+proc offsetTop*(e: Node): int {.importcpp: "#.offsetTop", nodecl.}
+proc offsetLeft*(e: Node): int {.importcpp: "#.offsetLeft", nodecl.}
diff --git a/karax/languages.nim b/karax/languages.nim
new file mode 100644
index 0000000..6b6b027
--- /dev/null
+++ b/karax/languages.nim
@@ -0,0 +1,616 @@
+## Karax -- Single page applications for Nim.
+
+type
+ Language* {.pure.} = enum ## A pure enum that contains most (all?)
+ ## known human languages in current use.
+ afZA ## "South Africa - Afrikaans"
+ amET ## "Ethiopia - Amharic"
+ arAE ## "U.A.E. - Arabic"
+ arBH ## "Bahrain - Arabic"
+ arDZ ## "Algeria - Arabic"
+ arEG ## "Egypt - Arabic"
+ arIQ ## "Iraq - Arabic"
+ arJO ## "Jordan - Arabic"
+ arKW ## "Kuwait - Arabic"
+ arLB ## "Lebanon - Arabic"
+ arLY ## "Libya - Arabic"
+ arMA ## "Morocco - Arabic"
+ arOM ## "Oman - Arabic"
+ arQA ## "Qatar - Arabic"
+ arSA ## "Saudi Arabia - Arabic"
+ arSY ## "Syria - Arabic"
+ arTN ## "Tunisia - Arabic"
+ arYE ## "Yemen - Arabic"
+ arnCL ## "Chile - Mapudungun"
+ asIN ## "India - Assamese"
+ azAZ ## "Azerbaijan - Azeri (Cyrillic)"
+ baRU ## "Russia - Bashkir"
+ beBY ## "Belarus - Belarusian"
+ bgBG ## "Bulgaria - Bulgarian"
+ bnBD ## "Bangladesh - Bengali"
+ bnIN ## "India - Bengali"
+ boCN ## "People's Republic of China - Tibetan"
+ brFR ## "France - Breton"
+ bsBA ## "Bosnia and Herzegovina - Bosnian (Cyrillic)"
+ caES ## "Spain - Catalan"
+ coFR ## "France - Corsican"
+ csCZ ## "Czech Republic - Czech"
+ cyGB ## "United Kingdom - Welsh"
+ daDK ## "Denmark - Danish"
+ deAT ## "Austria - German"
+ deCH ## "Switzerland - German"
+ deDE ## "Germany - German"
+ deLI ## "Liechtenstein - German"
+ deLU ## "Luxembourg - German"
+ dsbDE ## "Germany - Lower Sorbian"
+ dvMV ## "Maldives - Divehi"
+ elGR ## "Greece - Greek"
+ en029 ## "Caribbean - English"
+ enAU ## "Australia - English"
+ enBZ ## "Belize - English"
+ enCA ## "Canada - English"
+ enGB ## "United Kingdom - English"
+ enIE ## "Ireland - English"
+ enIN ## "India - English"
+ enJM ## "Jamaica - English"
+ enMY ## "Malaysia - English"
+ enNZ ## "New Zealand - English"
+ enPH ## "Republic of the Philippines - English"
+ enSG ## "Singapore - English"
+ enTT ## "Trinidad and Tobago - English"
+ enUS ## "United States - English"
+ enZA ## "South Africa - English"
+ enZW ## "Zimbabwe - English"
+ esAR ## "Argentina - Spanish"
+ esBO ## "Bolivia - Spanish"
+ esCL ## "Chile - Spanish"
+ esCO ## "Colombia - Spanish"
+ esCR ## "Costa Rica - Spanish"
+ esDO ## "Dominican Republic - Spanish"
+ esEC ## "Ecuador - Spanish"
+ esES ## "Spain - Spanish"
+ esGT ## "Guatemala - Spanish"
+ esHN ## "Honduras - Spanish"
+ esMX ## "Mexico - Spanish"
+ esNI ## "Nicaragua - Spanish"
+ esPA ## "Panama - Spanish"
+ esPE ## "Peru - Spanish"
+ esPR ## "Puerto Rico - Spanish"
+ esPY ## "Paraguay - Spanish"
+ esSV ## "El Salvador - Spanish"
+ esUS ## "United States - Spanish"
+ esUY ## "Uruguay - Spanish"
+ esVE ## "Bolivarian Republic of Venezuela - Spanish"
+ etEE ## "Estonia - Estonian"
+ euES ## "Spain - Basque"
+ faIR ## "Iran - Persian"
+ fiFI ## "Finland - Finnish"
+ filPH ## "Philippines - Filipino"
+ foFO ## "Faroe Islands - Faroese"
+ frBE ## "Belgium - French"
+ frCA ## "Canada - French"
+ frCH ## "Switzerland - French"
+ frFR ## "France - French"
+ frLU ## "Luxembourg - French"
+ frMC ## "Principality of Monaco - French"
+ fyNL ## "Netherlands - Frisian"
+ gaIE ## "Ireland - Irish"
+ gdGB ## "United Kingdom - Scottish Gaelic"
+ glES ## "Spain - Galician"
+ gswFR ## "France - Alsatian"
+ guIN ## "India - Gujarati"
+ haNG ## "Nigeria - Hausa (Latin)"
+ heIL ## "Israel - Hebrew"
+ hiIN ## "India - Hindi"
+ hrBA ## "Bosnia and Herzegovina - Croatian"
+ hrHR ## "Croatia - Croatian"
+ hsbDE ## "Germany - Upper Sorbian"
+ huHU ## "Hungary - Hungarian"
+ hyAM ## "Armenia - Armenian"
+ idID ## "Indonesia - Indonesian"
+ igNG ## "Nigeria - Igbo"
+ iiCN ## "People's Republic of China - Yi"
+ isIS ## "Iceland - Icelandic"
+ itCH ## "Switzerland - Italian"
+ itIT ## "Italy - Italian"
+ iuCA ## "Canada - Inuktitut (Latin)"
+ jaJP ## "Japan - Japanese"
+ kaGE ## "Georgia - Georgian"
+ kkKZ ## "Kazakhstan - Kazakh"
+ klGL ## "Greenland - Greenlandic"
+ kmKH ## "Cambodia - Khmer"
+ knIN ## "India - Kannada"
+ koKR ## "Korea - Korean"
+ kokIN ## "India - Konkani"
+ kyKG ## "Kyrgyzstan - Kyrgyz"
+ lbLU ## "Luxembourg - Luxembourgish"
+ loLA ## "Lao P.D.R. - Lao"
+ ltLT ## "Lithuania - Lithuanian"
+ lvLV ## "Latvia - Latvian"
+ miNZ ## "New Zealand - Maori"
+ mkMK ## "Macedonia (FYROM) - Macedonian (FYROM)"
+ mlIN ## "India - Malayalam"
+ mnCN ## "People's Republic of China - Mongolian (Traditional Mongolian)"
+ mnMN ## "Mongolia - Mongolian (Cyrillic)"
+ mohCA ## "Canada - Mohawk"
+ mrIN ## "India - Marathi"
+ msBN ## "Brunei Darussalam - Malay"
+ msMY ## "Malaysia - Malay"
+ mtMT ## "Malta - Maltese"
+ nbNO ## "Norway - Norwegian (Bokmal)"
+ neNP ## "Nepal - Nepali"
+ nlBE ## "Belgium - Dutch"
+ nlNL ## "Netherlands - Dutch"
+ nnNO ## "Norway - Norwegian (Nynorsk)"
+ nsoZA ## "South Africa - Sesotho sa Leboa"
+ ocFR ## "France - Occitan"
+ orIN ## "India - Oriya"
+ paIN ## "India - Punjabi"
+ plPL ## "Poland - Polish"
+ prsAF ## "Afghanistan - Dari"
+ psAF ## "Afghanistan - Pashto"
+ ptBR ## "Brazil - Portuguese"
+ ptPT ## "Portugal - Portuguese"
+ qutGT ## "Guatemala - K'iche"
+ quzBO ## "Bolivia - Quechua"
+ quzEC ## "Ecuador - Quechua"
+ quzPE ## "Peru - Quechua"
+ rmCH ## "Switzerland - Romansh"
+ roRO ## "Romania - Romanian"
+ ruRU ## "Russia - Russian"
+ rwRW ## "Rwanda - Kinyarwanda"
+ saIN ## "India - Sanskrit"
+ sahRU ## "Russia - Yakut"
+ seFI ## "Finland - Sami (Northern)"
+ seNO ## "Norway - Sami (Northern)"
+ seSE ## "Sweden - Sami (Northern)"
+ siLK ## "Sri Lanka - Sinhala"
+ skSK ## "Slovakia - Slovak"
+ slSI ## "Slovenia - Slovenian"
+ smaNO ## "Norway - Sami (Southern)"
+ smaSE ## "Sweden - Sami (Southern)"
+ smjNO ## "Norway - Sami (Lule)"
+ smjSE ## "Sweden - Sami (Lule)"
+ smnFI ## "Finland - Sami (Inari)"
+ smsFI ## "Finland - Sami (Skolt)"
+ sqAL ## "Albania - Albanian"
+ srBA ## "Bosnia and Herzegovina - Serbian (Cyrillic)"
+ srCS ## "Serbia and Montenegro (Former) - Serbian (Cyrillic)"
+ srME ## "Montenegro - Serbian (Cyrillic)"
+ srRS ## "Serbia - Serbian (Cyrillic)"
+ svFI ## "Finland - Swedish"
+ svSE ## "Sweden - Swedish"
+ swKE ## "Kenya - Kiswahili"
+ syrSY ## "Syria - Syriac"
+ taIN ## "India - Tamil"
+ teIN ## "India - Telugu"
+ tgTJ ## "Tajikistan - Tajik (Cyrillic)"
+ thTH ## "Thailand - Thai"
+ tkTM ## "Turkmenistan - Turkmen"
+ tnZA ## "South Africa - Setswana"
+ trTR ## "Turkey - Turkish"
+ ttRU ## "Russia - Tatar"
+ tzmDZ ## "Algeria - Tamazight (Latin)"
+ ugCN ## "People's Republic of China - Uyghur"
+ ukUA ## "Ukraine - Ukrainian"
+ urPK ## "Islamic Republic of Pakistan - Urdu"
+ uzUZ ## "Uzbekistan - Uzbek (Cyrillic)"
+ viVN ## "Vietnam - Vietnamese"
+ woSN ## "Senegal - Wolof"
+ xhZA ## "South Africa - isiXhosa"
+ yoNG ## "Nigeria - Yoruba"
+ zhCN ## "People's Republic of China - Chinese (Simplified) Legacy"
+ zhHK ## "Hong Kong S.A.R. - Chinese (Traditional) Legacy"
+ zhMO ## "Macao S.A.R. - Chinese (Traditional) Legacy"
+ zhSG ## "Singapore - Chinese (Simplified) Legacy"
+ zhTW ## "Taiwan - Chinese (Traditional) Legacy"
+ zuZA ## "South Africa - isiZulu"
+
+const
+ languageToName*: array[Language, cstring] = [
+ Language.afZA: cstring"South Africa - Afrikaans",
+ Language.amET: cstring"Ethiopia - Amharic",
+ Language.arAE: cstring"U.A.E. - Arabic",
+ Language.arBH: cstring"Bahrain - Arabic",
+ Language.arDZ: cstring"Algeria - Arabic",
+ Language.arEG: cstring"Egypt - Arabic",
+ Language.arIQ: cstring"Iraq - Arabic",
+ Language.arJO: cstring"Jordan - Arabic",
+ Language.arKW: cstring"Kuwait - Arabic",
+ Language.arLB: cstring"Lebanon - Arabic",
+ Language.arLY: cstring"Libya - Arabic",
+ Language.arMA: cstring"Morocco - Arabic",
+ Language.arOM: cstring"Oman - Arabic",
+ Language.arQA: cstring"Qatar - Arabic",
+ Language.arSA: cstring"Saudi Arabia - Arabic",
+ Language.arSY: cstring"Syria - Arabic",
+ Language.arTN: cstring"Tunisia - Arabic",
+ Language.arYE: cstring"Yemen - Arabic",
+ Language.arnCL: cstring"Chile - Mapudungun",
+ Language.asIN: cstring"India - Assamese",
+ Language.azAZ: cstring"Azerbaijan - Azeri (Cyrillic)",
+ Language.baRU: cstring"Russia - Bashkir",
+ Language.beBY: cstring"Belarus - Belarusian",
+ Language.bgBG: cstring"Bulgaria - Bulgarian",
+ Language.bnBD: cstring"Bangladesh - Bengali",
+ Language.bnIN: cstring"India - Bengali",
+ Language.boCN: cstring"People's Republic of China - Tibetan",
+ Language.brFR: cstring"France - Breton",
+ Language.bsBA: cstring"Bosnia and Herzegovina - Bosnian (Cyrillic)",
+ Language.caES: cstring"Spain - Catalan",
+ Language.coFR: cstring"France - Corsican",
+ Language.csCZ: cstring"Czech Republic - Czech",
+ Language.cyGB: cstring"United Kingdom - Welsh",
+ Language.daDK: cstring"Denmark - Danish",
+ Language.deAT: cstring"Austria - German",
+ Language.deCH: cstring"Switzerland - German",
+ Language.deDE: cstring"Germany - German",
+ Language.deLI: cstring"Liechtenstein - German",
+ Language.deLU: cstring"Luxembourg - German",
+ Language.dsbDE: cstring"Germany - Lower Sorbian",
+ Language.dvMV: cstring"Maldives - Divehi",
+ Language.elGR: cstring"Greece - Greek",
+ Language.en029: cstring"Caribbean - English",
+ Language.enAU: cstring"Australia - English",
+ Language.enBZ: cstring"Belize - English",
+ Language.enCA: cstring"Canada - English",
+ Language.enGB: cstring"United Kingdom - English",
+ Language.enIE: cstring"Ireland - English",
+ Language.enIN: cstring"India - English",
+ Language.enJM: cstring"Jamaica - English",
+ Language.enMY: cstring"Malaysia - English",
+ Language.enNZ: cstring"New Zealand - English",
+ Language.enPH: cstring"Republic of the Philippines - English",
+ Language.enSG: cstring"Singapore - English",
+ Language.enTT: cstring"Trinidad and Tobago - English",
+ Language.enUS: cstring"United States - English",
+ Language.enZA: cstring"South Africa - English",
+ Language.enZW: cstring"Zimbabwe - English",
+ Language.esAR: cstring"Argentina - Spanish",
+ Language.esBO: cstring"Bolivia - Spanish",
+ Language.esCL: cstring"Chile - Spanish",
+ Language.esCO: cstring"Colombia - Spanish",
+ Language.esCR: cstring"Costa Rica - Spanish",
+ Language.esDO: cstring"Dominican Republic - Spanish",
+ Language.esEC: cstring"Ecuador - Spanish",
+ Language.esES: cstring"Spain - Spanish",
+ Language.esGT: cstring"Guatemala - Spanish",
+ Language.esHN: cstring"Honduras - Spanish",
+ Language.esMX: cstring"Mexico - Spanish",
+ Language.esNI: cstring"Nicaragua - Spanish",
+ Language.esPA: cstring"Panama - Spanish",
+ Language.esPE: cstring"Peru - Spanish",
+ Language.esPR: cstring"Puerto Rico - Spanish",
+ Language.esPY: cstring"Paraguay - Spanish",
+ Language.esSV: cstring"El Salvador - Spanish",
+ Language.esUS: cstring"United States - Spanish",
+ Language.esUY: cstring"Uruguay - Spanish",
+ Language.esVE: cstring"Bolivarian Republic of Venezuela - Spanish",
+ Language.etEE: cstring"Estonia - Estonian",
+ Language.euES: cstring"Spain - Basque",
+ Language.faIR: cstring"Iran - Persian",
+ Language.fiFI: cstring"Finland - Finnish",
+ Language.filPH: cstring"Philippines - Filipino",
+ Language.foFO: cstring"Faroe Islands - Faroese",
+ Language.frBE: cstring"Belgium - French",
+ Language.frCA: cstring"Canada - French",
+ Language.frCH: cstring"Switzerland - French",
+ Language.frFR: cstring"France - French",
+ Language.frLU: cstring"Luxembourg - French",
+ Language.frMC: cstring"Principality of Monaco - French",
+ Language.fyNL: cstring"Netherlands - Frisian",
+ Language.gaIE: cstring"Ireland - Irish",
+ Language.gdGB: cstring"United Kingdom - Scottish Gaelic",
+ Language.glES: cstring"Spain - Galician",
+ Language.gswFR: cstring"France - Alsatian",
+ Language.guIN: cstring"India - Gujarati",
+ Language.haNG: cstring"Nigeria - Hausa (Latin)",
+ Language.heIL: cstring"Israel - Hebrew",
+ Language.hiIN: cstring"India - Hindi",
+ Language.hrBA: cstring"Bosnia and Herzegovina - Croatian",
+ Language.hrHR: cstring"Croatia - Croatian",
+ Language.hsbDE: cstring"Germany - Upper Sorbian",
+ Language.huHU: cstring"Hungary - Hungarian",
+ Language.hyAM: cstring"Armenia - Armenian",
+ Language.idID: cstring"Indonesia - Indonesian",
+ Language.igNG: cstring"Nigeria - Igbo",
+ Language.iiCN: cstring"People's Republic of China - Yi",
+ Language.isIS: cstring"Iceland - Icelandic",
+ Language.itCH: cstring"Switzerland - Italian",
+ Language.itIT: cstring"Italy - Italian",
+ Language.iuCA: cstring"Canada - Inuktitut (Latin)",
+ Language.jaJP: cstring"Japan - Japanese",
+ Language.kaGE: cstring"Georgia - Georgian",
+ Language.kkKZ: cstring"Kazakhstan - Kazakh",
+ Language.klGL: cstring"Greenland - Greenlandic",
+ Language.kmKH: cstring"Cambodia - Khmer",
+ Language.knIN: cstring"India - Kannada",
+ Language.koKR: cstring"Korea - Korean",
+ Language.kokIN: cstring"India - Konkani",
+ Language.kyKG: cstring"Kyrgyzstan - Kyrgyz",
+ Language.lbLU: cstring"Luxembourg - Luxembourgish",
+ Language.loLA: cstring"Lao P.D.R. - Lao",
+ Language.ltLT: cstring"Lithuania - Lithuanian",
+ Language.lvLV: cstring"Latvia - Latvian",
+ Language.miNZ: cstring"New Zealand - Maori",
+ Language.mkMK: cstring"Macedonia (FYROM) - Macedonian (FYROM)",
+ Language.mlIN: cstring"India - Malayalam",
+ Language.mnCN: cstring"People's Republic of China - Mongolian (Traditional Mongolian)",
+ Language.mnMN: cstring"Mongolia - Mongolian (Cyrillic)",
+ Language.mohCA: cstring"Canada - Mohawk",
+ Language.mrIN: cstring"India - Marathi",
+ Language.msBN: cstring"Brunei Darussalam - Malay",
+ Language.msMY: cstring"Malaysia - Malay",
+ Language.mtMT: cstring"Malta - Maltese",
+ Language.nbNO: cstring"Norway - Norwegian (Bokmal)",
+ Language.neNP: cstring"Nepal - Nepali",
+ Language.nlBE: cstring"Belgium - Dutch",
+ Language.nlNL: cstring"Netherlands - Dutch",
+ Language.nnNO: cstring"Norway - Norwegian (Nynorsk)",
+ Language.nsoZA: cstring"South Africa - Sesotho sa Leboa",
+ Language.ocFR: cstring"France - Occitan",
+ Language.orIN: cstring"India - Oriya",
+ Language.paIN: cstring"India - Punjabi",
+ Language.plPL: cstring"Poland - Polish",
+ Language.prsAF: cstring"Afghanistan - Dari",
+ Language.psAF: cstring"Afghanistan - Pashto",
+ Language.ptBR: cstring"Brazil - Portuguese",
+ Language.ptPT: cstring"Portugal - Portuguese",
+ Language.qutGT: cstring"Guatemala - K'iche",
+ Language.quzBO: cstring"Bolivia - Quechua",
+ Language.quzEC: cstring"Ecuador - Quechua",
+ Language.quzPE: cstring"Peru - Quechua",
+ Language.rmCH: cstring"Switzerland - Romansh",
+ Language.roRO: cstring"Romania - Romanian",
+ Language.ruRU: cstring"Russia - Russian",
+ Language.rwRW: cstring"Rwanda - Kinyarwanda",
+ Language.saIN: cstring"India - Sanskrit",
+ Language.sahRU: cstring"Russia - Yakut",
+ Language.seFI: cstring"Finland - Sami (Northern)",
+ Language.seNO: cstring"Norway - Sami (Northern)",
+ Language.seSE: cstring"Sweden - Sami (Northern)",
+ Language.siLK: cstring"Sri Lanka - Sinhala",
+ Language.skSK: cstring"Slovakia - Slovak",
+ Language.slSI: cstring"Slovenia - Slovenian",
+ Language.smaNO: cstring"Norway - Sami (Southern)",
+ Language.smaSE: cstring"Sweden - Sami (Southern)",
+ Language.smjNO: cstring"Norway - Sami (Lule)",
+ Language.smjSE: cstring"Sweden - Sami (Lule)",
+ Language.smnFI: cstring"Finland - Sami (Inari)",
+ Language.smsFI: cstring"Finland - Sami (Skolt)",
+ Language.sqAL: cstring"Albania - Albanian",
+ Language.srBA: cstring"Bosnia and Herzegovina - Serbian (Cyrillic)",
+ Language.srCS: cstring"Serbia and Montenegro (Former) - Serbian (Cyrillic)",
+ Language.srME: cstring"Montenegro - Serbian (Cyrillic)",
+ Language.srRS: cstring"Serbia - Serbian (Cyrillic)",
+ Language.svFI: cstring"Finland - Swedish",
+ Language.svSE: cstring"Sweden - Swedish",
+ Language.swKE: cstring"Kenya - Kiswahili",
+ Language.syrSY: cstring"Syria - Syriac",
+ Language.taIN: cstring"India - Tamil",
+ Language.teIN: cstring"India - Telugu",
+ Language.tgTJ: cstring"Tajikistan - Tajik (Cyrillic)",
+ Language.thTH: cstring"Thailand - Thai",
+ Language.tkTM: cstring"Turkmenistan - Turkmen",
+ Language.tnZA: cstring"South Africa - Setswana",
+ Language.trTR: cstring"Turkey - Turkish",
+ Language.ttRU: cstring"Russia - Tatar",
+ Language.tzmDZ: cstring"Algeria - Tamazight (Latin)",
+ Language.ugCN: cstring"People's Republic of China - Uyghur",
+ Language.ukUA: cstring"Ukraine - Ukrainian",
+ Language.urPK: cstring"Islamic Republic of Pakistan - Urdu",
+ Language.uzUZ: cstring"Uzbekistan - Uzbek (Cyrillic)",
+ Language.viVN: cstring"Vietnam - Vietnamese",
+ Language.woSN: cstring"Senegal - Wolof",
+ Language.xhZA: cstring"South Africa - isiXhosa",
+ Language.yoNG: cstring"Nigeria - Yoruba",
+ Language.zhCN: cstring"People's Republic of China - Chinese (Simplified) Legacy",
+ Language.zhHK: cstring"Hong Kong S.A.R. - Chinese (Traditional) Legacy",
+ Language.zhMO: cstring"Macao S.A.R. - Chinese (Traditional) Legacy",
+ Language.zhSG: cstring"Singapore - Chinese (Simplified) Legacy",
+ Language.zhTW: cstring"Taiwan - Chinese (Traditional) Legacy",
+ Language.zuZA: cstring"South Africa - isiZulu"]
+
+ languageToCode*: array[Language, cstring] = [
+ Language.afZA: cstring"af-ZA",
+ Language.amET: cstring"am-ET",
+ Language.arAE: cstring"ar-AE",
+ Language.arBH: cstring"ar-BH",
+ Language.arDZ: cstring"ar-DZ",
+ Language.arEG: cstring"ar-EG",
+ Language.arIQ: cstring"ar-IQ",
+ Language.arJO: cstring"ar-JO",
+ Language.arKW: cstring"ar-KW",
+ Language.arLB: cstring"ar-LB",
+ Language.arLY: cstring"ar-LY",
+ Language.arMA: cstring"ar-MA",
+ Language.arOM: cstring"ar-OM",
+ Language.arQA: cstring"ar-QA",
+ Language.arSA: cstring"ar-SA",
+ Language.arSY: cstring"ar-SY",
+ Language.arTN: cstring"ar-TN",
+ Language.arYE: cstring"ar-YE",
+ Language.arnCL: cstring"arn-CL",
+ Language.asIN: cstring"as-IN",
+ Language.azAZ: cstring"az-AZ",
+ Language.baRU: cstring"ba-RU",
+ Language.beBY: cstring"be-BY",
+ Language.bgBG: cstring"bg-BG",
+ Language.bnBD: cstring"bn-BD",
+ Language.bnIN: cstring"bn-IN",
+ Language.boCN: cstring"bo-CN",
+ Language.brFR: cstring"br-FR",
+ Language.bsBA: cstring"bs-BA",
+ Language.caES: cstring"ca-ES",
+ Language.coFR: cstring"co-FR",
+ Language.csCZ: cstring"cs-CZ",
+ Language.cyGB: cstring"cy-GB",
+ Language.daDK: cstring"da-DK",
+ Language.deAT: cstring"de-AT",
+ Language.deCH: cstring"de-CH",
+ Language.deDE: cstring"de-DE",
+ Language.deLI: cstring"de-LI",
+ Language.deLU: cstring"de-LU",
+ Language.dsbDE: cstring"dsb-DE",
+ Language.dvMV: cstring"dv-MV",
+ Language.elGR: cstring"el-GR",
+ Language.en029: cstring"en-029",
+ Language.enAU: cstring"en-AU",
+ Language.enBZ: cstring"en-BZ",
+ Language.enCA: cstring"en-CA",
+ Language.enGB: cstring"en-GB",
+ Language.enIE: cstring"en-IE",
+ Language.enIN: cstring"en-IN",
+ Language.enJM: cstring"en-JM",
+ Language.enMY: cstring"en-MY",
+ Language.enNZ: cstring"en-NZ",
+ Language.enPH: cstring"en-PH",
+ Language.enSG: cstring"en-SG",
+ Language.enTT: cstring"en-TT",
+ Language.enUS: cstring"en-US",
+ Language.enZA: cstring"en-ZA",
+ Language.enZW: cstring"en-ZW",
+ Language.esAR: cstring"es-AR",
+ Language.esBO: cstring"es-BO",
+ Language.esCL: cstring"es-CL",
+ Language.esCO: cstring"es-CO",
+ Language.esCR: cstring"es-CR",
+ Language.esDO: cstring"es-DO",
+ Language.esEC: cstring"es-EC",
+ Language.esES: cstring"es-ES",
+ Language.esGT: cstring"es-GT",
+ Language.esHN: cstring"es-HN",
+ Language.esMX: cstring"es-MX",
+ Language.esNI: cstring"es-NI",
+ Language.esPA: cstring"es-PA",
+ Language.esPE: cstring"es-PE",
+ Language.esPR: cstring"es-PR",
+ Language.esPY: cstring"es-PY",
+ Language.esSV: cstring"es-SV",
+ Language.esUS: cstring"es-US",
+ Language.esUY: cstring"es-UY",
+ Language.esVE: cstring"es-VE",
+ Language.etEE: cstring"et-EE",
+ Language.euES: cstring"eu-ES",
+ Language.faIR: cstring"fa-IR",
+ Language.fiFI: cstring"fi-FI",
+ Language.filPH: cstring"fil-PH",
+ Language.foFO: cstring"fo-FO",
+ Language.frBE: cstring"fr-BE",
+ Language.frCA: cstring"fr-CA",
+ Language.frCH: cstring"fr-CH",
+ Language.frFR: cstring"fr-FR",
+ Language.frLU: cstring"fr-LU",
+ Language.frMC: cstring"fr-MC",
+ Language.fyNL: cstring"fy-NL",
+ Language.gaIE: cstring"ga-IE",
+ Language.gdGB: cstring"gd-GB",
+ Language.glES: cstring"gl-ES",
+ Language.gswFR: cstring"gsw-FR",
+ Language.guIN: cstring"gu-IN",
+ Language.haNG: cstring"ha-NG",
+ Language.heIL: cstring"he-IL",
+ Language.hiIN: cstring"hi-IN",
+ Language.hrBA: cstring"hr-BA",
+ Language.hrHR: cstring"hr-HR",
+ Language.hsbDE: cstring"hsb-DE",
+ Language.huHU: cstring"hu-HU",
+ Language.hyAM: cstring"hy-AM",
+ Language.idID: cstring"id-ID",
+ Language.igNG: cstring"ig-NG",
+ Language.iiCN: cstring"ii-CN",
+ Language.isIS: cstring"is-IS",
+ Language.itCH: cstring"it-CH",
+ Language.itIT: cstring"it-IT",
+ Language.iuCA: cstring"iu-CA",
+ Language.jaJP: cstring"ja-JP",
+ Language.kaGE: cstring"ka-GE",
+ Language.kkKZ: cstring"kk-KZ",
+ Language.klGL: cstring"kl-GL",
+ Language.kmKH: cstring"km-KH",
+ Language.knIN: cstring"kn-IN",
+ Language.koKR: cstring"ko-KR",
+ Language.kokIN: cstring"kok-IN",
+ Language.kyKG: cstring"ky-KG",
+ Language.lbLU: cstring"lb-LU",
+ Language.loLA: cstring"lo-LA",
+ Language.ltLT: cstring"lt-LT",
+ Language.lvLV: cstring"lv-LV",
+ Language.miNZ: cstring"mi-NZ",
+ Language.mkMK: cstring"mk-MK",
+ Language.mlIN: cstring"ml-IN",
+ Language.mnCN: cstring"mn-CN",
+ Language.mnMN: cstring"mn-MN",
+ Language.mohCA: cstring"moh-CA",
+ Language.mrIN: cstring"mr-IN",
+ Language.msBN: cstring"ms-BN",
+ Language.msMY: cstring"ms-MY",
+ Language.mtMT: cstring"mt-MT",
+ Language.nbNO: cstring"nb-NO",
+ Language.neNP: cstring"ne-NP",
+ Language.nlBE: cstring"nl-BE",
+ Language.nlNL: cstring"nl-NL",
+ Language.nnNO: cstring"nn-NO",
+ Language.nsoZA: cstring"nso-ZA",
+ Language.ocFR: cstring"oc-FR",
+ Language.orIN: cstring"or-IN",
+ Language.paIN: cstring"pa-IN",
+ Language.plPL: cstring"pl-PL",
+ Language.prsAF: cstring"prs-AF",
+ Language.psAF: cstring"ps-AF",
+ Language.ptBR: cstring"pt-BR",
+ Language.ptPT: cstring"pt-PT",
+ Language.qutGT: cstring"qut-GT",
+ Language.quzBO: cstring"quz-BO",
+ Language.quzEC: cstring"quz-EC",
+ Language.quzPE: cstring"quz-PE",
+ Language.rmCH: cstring"rm-CH",
+ Language.roRO: cstring"ro-RO",
+ Language.ruRU: cstring"ru-RU",
+ Language.rwRW: cstring"rw-RW",
+ Language.saIN: cstring"sa-IN",
+ Language.sahRU: cstring"sah-RU",
+ Language.seFI: cstring"se-FI",
+ Language.seNO: cstring"se-NO",
+ Language.seSE: cstring"se-SE",
+ Language.siLK: cstring"si-LK",
+ Language.skSK: cstring"sk-SK",
+ Language.slSI: cstring"sl-SI",
+ Language.smaNO: cstring"sma-NO",
+ Language.smaSE: cstring"sma-SE",
+ Language.smjNO: cstring"smj-NO",
+ Language.smjSE: cstring"smj-SE",
+ Language.smnFI: cstring"smn-FI",
+ Language.smsFI: cstring"sms-FI",
+ Language.sqAL: cstring"sq-AL",
+ Language.srBA: cstring"sr-BA",
+ Language.srCS: cstring"sr-CS",
+ Language.srME: cstring"sr-ME",
+ Language.srRS: cstring"sr-RS",
+ Language.svFI: cstring"sv-FI",
+ Language.svSE: cstring"sv-SE",
+ Language.swKE: cstring"sw-KE",
+ Language.syrSY: cstring"syr-SY",
+ Language.taIN: cstring"ta-IN",
+ Language.teIN: cstring"te-IN",
+ Language.tgTJ: cstring"tg-TJ",
+ Language.thTH: cstring"th-TH",
+ Language.tkTM: cstring"tk-TM",
+ Language.tnZA: cstring"tn-ZA",
+ Language.trTR: cstring"tr-TR",
+ Language.ttRU: cstring"tt-RU",
+ Language.tzmDZ: cstring"tzm-DZ",
+ Language.ugCN: cstring"ug-CN",
+ Language.ukUA: cstring"uk-UA",
+ Language.urPK: cstring"ur-PK",
+ Language.uzUZ: cstring"uz-UZ",
+ Language.viVN: cstring"vi-VN",
+ Language.woSN: cstring"wo-SN",
+ Language.xhZA: cstring"xh-ZA",
+ Language.yoNG: cstring"yo-NG",
+ Language.zhCN: cstring"zh-CN",
+ Language.zhHK: cstring"zh-HK",
+ Language.zhMO: cstring"zh-MO",
+ Language.zhSG: cstring"zh-SG",
+ Language.zhTW: cstring"zh-TW",
+ Language.zuZA: cstring"zu-ZA"]
diff --git a/karax/localstorage.nim b/karax/localstorage.nim
new file mode 100644
index 0000000..e2fb62e
--- /dev/null
+++ b/karax/localstorage.nim
@@ -0,0 +1,10 @@
+## Karax -- Single page applications for Nim.
+
+## This module contains wrappers for the HTML 5 local storage.
+
+proc getItem*(key: cstring): cstring {.importc: "localStorage.getItem".}
+proc setItem*(key, value: cstring) {.importc: "localStorage.setItem".}
+proc hasItem*(key: cstring): bool {.importcpp: "(localStorage.getItem(#) !== null)".}
+proc clear*() {.importc: "localStorage.clear".}
+
+proc removeItem*(key: cstring) {.importc: "localStorage.removeItem".}
diff --git a/karax/lookuptables.nim b/karax/lookuptables.nim
new file mode 100644
index 0000000..1a1d51d
--- /dev/null
+++ b/karax/lookuptables.nim
@@ -0,0 +1,38 @@
+
+import macros, kbase
+from strutils import `%`
+
+macro fieldNamesAsArray*(t: typed; pattern = "$1"): untyped =
+ var impl = getTypeImpl(getTypeImpl(t)[1])
+ if impl.kind == nnkRefTy: impl = getTypeImpl(impl[0])
+ expectKind impl, nnkObjectTy
+ impl = impl[2]
+ expectKind impl, nnkRecList
+ result = newTree(nnkBracket)
+ for x in impl:
+ expectKind x, nnkIdentDefs
+ result.add(newCall(bindSym"kstring", newLit(pattern.strVal % $x[0])))
+
+proc str(n: NimNode): NimNode =
+ if n.kind in {nnkStrLit, nnkTripleStrLit}:
+ result = newCall(bindSym"kstring", n)
+ else:
+ result = copyNimNode(n)
+ for i in 0..= 0:
+ x.s.delete(position)
+ x.broadcast(Mark)
+ x.broadcast(Deleted, position)
+
+when false:
+ proc `:=`[T](x: Reactive[T], f: proc(): T) =
+ toTrack = wrapObserver(f())
+ x.value = f()
+ toTrack = nil
+
+ proc map*[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] =
+ let xl = x.L.value
+ let res = newRSeq[U](xl)
+ for i in 0..
+
+"""
+
+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
new file mode 100644
index 0000000..8983496
--- /dev/null
+++ b/karax/tools/karun.nims
@@ -0,0 +1 @@
+switch("threads", "on")
\ No newline at end of file
diff --git a/karax/tools/static_server.nim b/karax/tools/static_server.nim
new file mode 100644
index 0000000..eeddf4f
--- /dev/null
+++ b/karax/tools/static_server.nim
@@ -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()
diff --git a/karax/vdom.nim b/karax/vdom.nim
new file mode 100644
index 0000000..8fd654e
--- /dev/null
+++ b/karax/vdom.nim
@@ -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.. 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("<")
+ of '>': result.add(">")
+ of '&': result.add("&")
+ of '"': result.add(""")
+ 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("<")
+ of '>': result.add(">")
+ of '&': result.add("&")
+ of '"': result.add(""")
+ of '\'': result.add("'")
+ of '/': result.add("/")
+ 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..b`` is
+ # different from ``a b``.
+ for i in 0..")
+ 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.. 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..`` 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
deleted file mode 100644
index c08ec99..0000000
--- a/readme.rst
+++ /dev/null
@@ -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.
diff --git a/src/dom.nim b/src/dom.nim
deleted file mode 100644
index 9414c89..0000000
--- a/src/dom.nim
+++ /dev/null
@@ -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
-## `_.
-
-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
diff --git a/src/jdict.nim b/src/jdict.nim
deleted file mode 100644
index 0eecb58..0000000
--- a/src/jdict.nim
+++ /dev/null
@@ -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 #[#]".}
-
diff --git a/src/karax.nim b/src/karax.nim
deleted file mode 100644
index f0f70dd..0000000
--- a/src/karax.nim
+++ /dev/null
@@ -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.. 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.. 0:
- plugins[0][1]()
-
diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim
deleted file mode 100644
index 6b8e540..0000000
--- a/src/karaxdsl.nim
+++ /dev/null
@@ -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.. 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
diff --git a/src/vdom.nim b/src/vdom.nim
deleted file mode 100644
index 0ddfc3a..0000000
--- a/src/vdom.nim
+++ /dev/null
@@ -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.. 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)
diff --git a/tests/blur.nim b/tests/blur.nim
new file mode 100644
index 0000000..fbb0a46
--- /dev/null
+++ b/tests/blur.nim
@@ -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
diff --git a/tests/compiler_tests.nim b/tests/compiler_tests.nim
new file mode 100644
index 0000000..fec1eb0
--- /dev/null
+++ b/tests/compiler_tests.nim
@@ -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"
+
diff --git a/tests/components.nim b/tests/components.nim
new file mode 100644
index 0000000..6024689
--- /dev/null
+++ b/tests/components.nim
@@ -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
diff --git a/tests/diffDomTests.html b/tests/diffDomTests.html
index 5ecf130..f8f0e18 100644
--- a/tests/diffDomTests.html
+++ b/tests/diffDomTests.html
@@ -3,14 +3,13 @@
Diff dom tests
-
-
+
diff --git a/tests/diffDomTests.nim b/tests/diffDomTests.nim
index 734a328..56f8b9f 100644
--- a/tests/diffDomTests.nim
+++ b/tests/diffDomTests.nim
@@ -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()
diff --git a/tests/difftest.nim b/tests/difftest.nim
new file mode 100644
index 0000000..aa2a3c0
--- /dev/null
+++ b/tests/difftest.nim
@@ -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..= 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
diff --git a/tests/domEventTests.css b/tests/domEventTests.css
new file mode 100644
index 0000000..f590741
--- /dev/null
+++ b/tests/domEventTests.css
@@ -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;
+}
\ No newline at end of file
diff --git a/tests/domEventTests.html b/tests/domEventTests.html
new file mode 100644
index 0000000..dbbff0d
--- /dev/null
+++ b/tests/domEventTests.html
@@ -0,0 +1,13 @@
+
+
+
+ DOM Event Tests
+
+
+
+
+
+
+
+
+
diff --git a/tests/domEventTests.nim b/tests/domEventTests.nim
new file mode 100644
index 0000000..347d636
--- /dev/null
+++ b/tests/domEventTests.nim
@@ -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()
diff --git a/tests/jquery-3.2.1.js b/tests/jquery-3.2.1.js
new file mode 100644
index 0000000..d2d8ca4
--- /dev/null
+++ b/tests/jquery-3.2.1.js
@@ -0,0 +1,10253 @@
+/*!
+ * jQuery JavaScript Library v3.2.1
+ * https://jquery.com/
+ *
+ * Includes Sizzle.js
+ * https://sizzlejs.com/
+ *
+ * Copyright JS Foundation and other contributors
+ * Released under the MIT license
+ * https://jquery.org/license
+ *
+ * Date: 2017-03-20T18:59Z
+ */
+( function( global, factory ) {
+
+ "use strict";
+
+ if ( typeof module === "object" && typeof module.exports === "object" ) {
+
+ // For CommonJS and CommonJS-like environments where a proper `window`
+ // is present, execute the factory and get jQuery.
+ // For environments that do not have a `window` with a `document`
+ // (such as Node.js), expose a factory as module.exports.
+ // This accentuates the need for the creation of a real `window`.
+ // e.g. var jQuery = require("jquery")(window);
+ // See ticket #14549 for more info.
+ module.exports = global.document ?
+ factory( global, true ) :
+ function( w ) {
+ if ( !w.document ) {
+ throw new Error( "jQuery requires a window with a document" );
+ }
+ return factory( w );
+ };
+ } else {
+ factory( global );
+ }
+
+// Pass this if window is not defined yet
+} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
+
+// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1
+// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode
+// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common
+// enough that all such attempts are guarded in a try block.
+"use strict";
+
+var arr = [];
+
+var document = window.document;
+
+var getProto = Object.getPrototypeOf;
+
+var slice = arr.slice;
+
+var concat = arr.concat;
+
+var push = arr.push;
+
+var indexOf = arr.indexOf;
+
+var class2type = {};
+
+var toString = class2type.toString;
+
+var hasOwn = class2type.hasOwnProperty;
+
+var fnToString = hasOwn.toString;
+
+var ObjectFunctionString = fnToString.call( Object );
+
+var support = {};
+
+
+
+ function DOMEval( code, doc ) {
+ doc = doc || document;
+
+ var script = doc.createElement( "script" );
+
+ script.text = code;
+ doc.head.appendChild( script ).parentNode.removeChild( script );
+ }
+/* global Symbol */
+// Defining this global in .eslintrc.json would create a danger of using the global
+// unguarded in another place, it seems safer to define global only for this module
+
+
+
+var
+ version = "3.2.1",
+
+ // Define a local copy of jQuery
+ jQuery = function( selector, context ) {
+
+ // The jQuery object is actually just the init constructor 'enhanced'
+ // Need init if jQuery is called (just allow error to be thrown if not included)
+ return new jQuery.fn.init( selector, context );
+ },
+
+ // Support: Android <=4.0 only
+ // Make sure we trim BOM and NBSP
+ rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,
+
+ // Matches dashed string for camelizing
+ rmsPrefix = /^-ms-/,
+ rdashAlpha = /-([a-z])/g,
+
+ // Used by jQuery.camelCase as callback to replace()
+ fcamelCase = function( all, letter ) {
+ return letter.toUpperCase();
+ };
+
+jQuery.fn = jQuery.prototype = {
+
+ // The current version of jQuery being used
+ jquery: version,
+
+ constructor: jQuery,
+
+ // The default length of a jQuery object is 0
+ length: 0,
+
+ toArray: function() {
+ return slice.call( this );
+ },
+
+ // Get the Nth element in the matched element set OR
+ // Get the whole matched element set as a clean array
+ get: function( num ) {
+
+ // Return all the elements in a clean array
+ if ( num == null ) {
+ return slice.call( this );
+ }
+
+ // Return just the one element from the set
+ return num < 0 ? this[ num + this.length ] : this[ num ];
+ },
+
+ // Take an array of elements and push it onto the stack
+ // (returning the new matched element set)
+ pushStack: function( elems ) {
+
+ // Build a new jQuery matched element set
+ var ret = jQuery.merge( this.constructor(), elems );
+
+ // Add the old object onto the stack (as a reference)
+ ret.prevObject = this;
+
+ // Return the newly-formed element set
+ return ret;
+ },
+
+ // Execute a callback for every element in the matched set.
+ each: function( callback ) {
+ return jQuery.each( this, callback );
+ },
+
+ map: function( callback ) {
+ return this.pushStack( jQuery.map( this, function( elem, i ) {
+ return callback.call( elem, i, elem );
+ } ) );
+ },
+
+ slice: function() {
+ return this.pushStack( slice.apply( this, arguments ) );
+ },
+
+ first: function() {
+ return this.eq( 0 );
+ },
+
+ last: function() {
+ return this.eq( -1 );
+ },
+
+ eq: function( i ) {
+ var len = this.length,
+ j = +i + ( i < 0 ? len : 0 );
+ return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] );
+ },
+
+ end: function() {
+ return this.prevObject || this.constructor();
+ },
+
+ // For internal use only.
+ // Behaves like an Array's method, not like a jQuery method.
+ push: push,
+ sort: arr.sort,
+ splice: arr.splice
+};
+
+jQuery.extend = jQuery.fn.extend = function() {
+ var options, name, src, copy, copyIsArray, clone,
+ target = arguments[ 0 ] || {},
+ i = 1,
+ length = arguments.length,
+ deep = false;
+
+ // Handle a deep copy situation
+ if ( typeof target === "boolean" ) {
+ deep = target;
+
+ // Skip the boolean and the target
+ target = arguments[ i ] || {};
+ i++;
+ }
+
+ // Handle case when target is a string or something (possible in deep copy)
+ if ( typeof target !== "object" && !jQuery.isFunction( target ) ) {
+ target = {};
+ }
+
+ // Extend jQuery itself if only one argument is passed
+ if ( i === length ) {
+ target = this;
+ i--;
+ }
+
+ for ( ; i < length; i++ ) {
+
+ // Only deal with non-null/undefined values
+ if ( ( options = arguments[ i ] ) != null ) {
+
+ // Extend the base object
+ for ( name in options ) {
+ src = target[ name ];
+ copy = options[ name ];
+
+ // Prevent never-ending loop
+ if ( target === copy ) {
+ continue;
+ }
+
+ // Recurse if we're merging plain objects or arrays
+ if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
+ ( copyIsArray = Array.isArray( copy ) ) ) ) {
+
+ if ( copyIsArray ) {
+ copyIsArray = false;
+ clone = src && Array.isArray( src ) ? src : [];
+
+ } else {
+ clone = src && jQuery.isPlainObject( src ) ? src : {};
+ }
+
+ // Never move original objects, clone them
+ target[ name ] = jQuery.extend( deep, clone, copy );
+
+ // Don't bring in undefined values
+ } else if ( copy !== undefined ) {
+ target[ name ] = copy;
+ }
+ }
+ }
+ }
+
+ // Return the modified object
+ return target;
+};
+
+jQuery.extend( {
+
+ // Unique for each copy of jQuery on the page
+ expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ),
+
+ // Assume jQuery is ready without the ready module
+ isReady: true,
+
+ error: function( msg ) {
+ throw new Error( msg );
+ },
+
+ noop: function() {},
+
+ isFunction: function( obj ) {
+ return jQuery.type( obj ) === "function";
+ },
+
+ isWindow: function( obj ) {
+ return obj != null && obj === obj.window;
+ },
+
+ isNumeric: function( obj ) {
+
+ // As of jQuery 3.0, isNumeric is limited to
+ // strings and numbers (primitives or objects)
+ // that can be coerced to finite numbers (gh-2662)
+ var type = jQuery.type( obj );
+ return ( type === "number" || type === "string" ) &&
+
+ // parseFloat NaNs numeric-cast false positives ("")
+ // ...but misinterprets leading-number strings, particularly hex literals ("0x...")
+ // subtraction forces infinities to NaN
+ !isNaN( obj - parseFloat( obj ) );
+ },
+
+ isPlainObject: function( obj ) {
+ var proto, Ctor;
+
+ // Detect obvious negatives
+ // Use toString instead of jQuery.type to catch host objects
+ if ( !obj || toString.call( obj ) !== "[object Object]" ) {
+ return false;
+ }
+
+ proto = getProto( obj );
+
+ // Objects with no prototype (e.g., `Object.create( null )`) are plain
+ if ( !proto ) {
+ return true;
+ }
+
+ // Objects with prototype are plain iff they were constructed by a global Object function
+ Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor;
+ return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString;
+ },
+
+ isEmptyObject: function( obj ) {
+
+ /* eslint-disable no-unused-vars */
+ // See https://github.com/eslint/eslint/issues/6125
+ var name;
+
+ for ( name in obj ) {
+ return false;
+ }
+ return true;
+ },
+
+ type: function( obj ) {
+ if ( obj == null ) {
+ return obj + "";
+ }
+
+ // Support: Android <=2.3 only (functionish RegExp)
+ return typeof obj === "object" || typeof obj === "function" ?
+ class2type[ toString.call( obj ) ] || "object" :
+ typeof obj;
+ },
+
+ // Evaluates a script in a global context
+ globalEval: function( code ) {
+ DOMEval( code );
+ },
+
+ // Convert dashed to camelCase; used by the css and data modules
+ // Support: IE <=9 - 11, Edge 12 - 13
+ // Microsoft forgot to hump their vendor prefix (#9572)
+ camelCase: function( string ) {
+ return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
+ },
+
+ each: function( obj, callback ) {
+ var length, i = 0;
+
+ if ( isArrayLike( obj ) ) {
+ length = obj.length;
+ for ( ; i < length; i++ ) {
+ if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
+ break;
+ }
+ }
+ } else {
+ for ( i in obj ) {
+ if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
+ break;
+ }
+ }
+ }
+
+ return obj;
+ },
+
+ // Support: Android <=4.0 only
+ trim: function( text ) {
+ return text == null ?
+ "" :
+ ( text + "" ).replace( rtrim, "" );
+ },
+
+ // results is for internal usage only
+ makeArray: function( arr, results ) {
+ var ret = results || [];
+
+ if ( arr != null ) {
+ if ( isArrayLike( Object( arr ) ) ) {
+ jQuery.merge( ret,
+ typeof arr === "string" ?
+ [ arr ] : arr
+ );
+ } else {
+ push.call( ret, arr );
+ }
+ }
+
+ return ret;
+ },
+
+ inArray: function( elem, arr, i ) {
+ return arr == null ? -1 : indexOf.call( arr, elem, i );
+ },
+
+ // Support: Android <=4.0 only, PhantomJS 1 only
+ // push.apply(_, arraylike) throws on ancient WebKit
+ merge: function( first, second ) {
+ var len = +second.length,
+ j = 0,
+ i = first.length;
+
+ for ( ; j < len; j++ ) {
+ first[ i++ ] = second[ j ];
+ }
+
+ first.length = i;
+
+ return first;
+ },
+
+ grep: function( elems, callback, invert ) {
+ var callbackInverse,
+ matches = [],
+ i = 0,
+ length = elems.length,
+ callbackExpect = !invert;
+
+ // Go through the array, only saving the items
+ // that pass the validator function
+ for ( ; i < length; i++ ) {
+ callbackInverse = !callback( elems[ i ], i );
+ if ( callbackInverse !== callbackExpect ) {
+ matches.push( elems[ i ] );
+ }
+ }
+
+ return matches;
+ },
+
+ // arg is for internal usage only
+ map: function( elems, callback, arg ) {
+ var length, value,
+ i = 0,
+ ret = [];
+
+ // Go through the array, translating each of the items to their new values
+ if ( isArrayLike( elems ) ) {
+ length = elems.length;
+ for ( ; i < length; i++ ) {
+ value = callback( elems[ i ], i, arg );
+
+ if ( value != null ) {
+ ret.push( value );
+ }
+ }
+
+ // Go through every key on the object,
+ } else {
+ for ( i in elems ) {
+ value = callback( elems[ i ], i, arg );
+
+ if ( value != null ) {
+ ret.push( value );
+ }
+ }
+ }
+
+ // Flatten any nested arrays
+ return concat.apply( [], ret );
+ },
+
+ // A global GUID counter for objects
+ guid: 1,
+
+ // Bind a function to a context, optionally partially applying any
+ // arguments.
+ proxy: function( fn, context ) {
+ var tmp, args, proxy;
+
+ if ( typeof context === "string" ) {
+ tmp = fn[ context ];
+ context = fn;
+ fn = tmp;
+ }
+
+ // Quick check to determine if target is callable, in the spec
+ // this throws a TypeError, but we will just return undefined.
+ if ( !jQuery.isFunction( fn ) ) {
+ return undefined;
+ }
+
+ // Simulated bind
+ args = slice.call( arguments, 2 );
+ proxy = function() {
+ return fn.apply( context || this, args.concat( slice.call( arguments ) ) );
+ };
+
+ // Set the guid of unique handler to the same of original handler, so it can be removed
+ proxy.guid = fn.guid = fn.guid || jQuery.guid++;
+
+ return proxy;
+ },
+
+ now: Date.now,
+
+ // jQuery.support is not used in Core but other projects attach their
+ // properties to it so it needs to exist.
+ support: support
+} );
+
+if ( typeof Symbol === "function" ) {
+ jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ];
+}
+
+// Populate the class2type map
+jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ),
+function( i, name ) {
+ class2type[ "[object " + name + "]" ] = name.toLowerCase();
+} );
+
+function isArrayLike( obj ) {
+
+ // Support: real iOS 8.2 only (not reproducible in simulator)
+ // `in` check used to prevent JIT error (gh-2145)
+ // hasOwn isn't used here due to false negatives
+ // regarding Nodelist length in IE
+ var length = !!obj && "length" in obj && obj.length,
+ type = jQuery.type( obj );
+
+ if ( type === "function" || jQuery.isWindow( obj ) ) {
+ return false;
+ }
+
+ return type === "array" || length === 0 ||
+ typeof length === "number" && length > 0 && ( length - 1 ) in obj;
+}
+var Sizzle =
+/*!
+ * Sizzle CSS Selector Engine v2.3.3
+ * https://sizzlejs.com/
+ *
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license
+ * http://jquery.org/license
+ *
+ * Date: 2016-08-08
+ */
+(function( window ) {
+
+var i,
+ support,
+ Expr,
+ getText,
+ isXML,
+ tokenize,
+ compile,
+ select,
+ outermostContext,
+ sortInput,
+ hasDuplicate,
+
+ // Local document vars
+ setDocument,
+ document,
+ docElem,
+ documentIsHTML,
+ rbuggyQSA,
+ rbuggyMatches,
+ matches,
+ contains,
+
+ // Instance-specific data
+ expando = "sizzle" + 1 * new Date(),
+ preferredDoc = window.document,
+ dirruns = 0,
+ done = 0,
+ classCache = createCache(),
+ tokenCache = createCache(),
+ compilerCache = createCache(),
+ sortOrder = function( a, b ) {
+ if ( a === b ) {
+ hasDuplicate = true;
+ }
+ return 0;
+ },
+
+ // Instance methods
+ hasOwn = ({}).hasOwnProperty,
+ arr = [],
+ pop = arr.pop,
+ push_native = arr.push,
+ push = arr.push,
+ slice = arr.slice,
+ // Use a stripped-down indexOf as it's faster than native
+ // https://jsperf.com/thor-indexof-vs-for/5
+ indexOf = function( list, elem ) {
+ var i = 0,
+ len = list.length;
+ for ( ; i < len; i++ ) {
+ if ( list[i] === elem ) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",
+
+ // Regular expressions
+
+ // http://www.w3.org/TR/css3-selectors/#whitespace
+ whitespace = "[\\x20\\t\\r\\n\\f]",
+
+ // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
+ identifier = "(?:\\\\.|[\\w-]|[^\0-\\xa0])+",
+
+ // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors
+ attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace +
+ // Operator (capture 2)
+ "*([*^$|!~]?=)" + whitespace +
+ // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]"
+ "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace +
+ "*\\]",
+
+ pseudos = ":(" + identifier + ")(?:\\((" +
+ // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:
+ // 1. quoted (capture 3; capture 4 or capture 5)
+ "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" +
+ // 2. simple (capture 6)
+ "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" +
+ // 3. anything else (capture 2)
+ ".*" +
+ ")\\)|)",
+
+ // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
+ rwhitespace = new RegExp( whitespace + "+", "g" ),
+ rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ),
+
+ rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
+ rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ),
+
+ rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ),
+
+ rpseudo = new RegExp( pseudos ),
+ ridentifier = new RegExp( "^" + identifier + "$" ),
+
+ matchExpr = {
+ "ID": new RegExp( "^#(" + identifier + ")" ),
+ "CLASS": new RegExp( "^\\.(" + identifier + ")" ),
+ "TAG": new RegExp( "^(" + identifier + "|[*])" ),
+ "ATTR": new RegExp( "^" + attributes ),
+ "PSEUDO": new RegExp( "^" + pseudos ),
+ "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace +
+ "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace +
+ "*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
+ "bool": new RegExp( "^(?:" + booleans + ")$", "i" ),
+ // For use in libraries implementing .is()
+ // We use this for POS matching in `select`
+ "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" +
+ whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
+ },
+
+ rinputs = /^(?:input|select|textarea|button)$/i,
+ rheader = /^h\d$/i,
+
+ rnative = /^[^{]+\{\s*\[native \w/,
+
+ // Easily-parseable/retrievable ID or TAG or CLASS selectors
+ rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,
+
+ rsibling = /[+~]/,
+
+ // CSS escapes
+ // http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
+ runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ),
+ funescape = function( _, escaped, escapedWhitespace ) {
+ var high = "0x" + escaped - 0x10000;
+ // NaN means non-codepoint
+ // Support: Firefox<24
+ // Workaround erroneous numeric interpretation of +"0x"
+ return high !== high || escapedWhitespace ?
+ escaped :
+ high < 0 ?
+ // BMP codepoint
+ String.fromCharCode( high + 0x10000 ) :
+ // Supplemental Plane codepoint (surrogate pair)
+ String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
+ },
+
+ // CSS string/identifier serialization
+ // https://drafts.csswg.org/cssom/#common-serializing-idioms
+ rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,
+ fcssescape = function( ch, asCodePoint ) {
+ if ( asCodePoint ) {
+
+ // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER
+ if ( ch === "\0" ) {
+ return "\uFFFD";
+ }
+
+ // Control characters and (dependent upon position) numbers get escaped as code points
+ return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " ";
+ }
+
+ // Other potentially-special ASCII characters get backslash-escaped
+ return "\\" + ch;
+ },
+
+ // Used for iframes
+ // See setDocument()
+ // Removing the function wrapper causes a "Permission Denied"
+ // error in IE
+ unloadHandler = function() {
+ setDocument();
+ },
+
+ disabledAncestor = addCombinator(
+ function( elem ) {
+ return elem.disabled === true && ("form" in elem || "label" in elem);
+ },
+ { dir: "parentNode", next: "legend" }
+ );
+
+// Optimize for push.apply( _, NodeList )
+try {
+ push.apply(
+ (arr = slice.call( preferredDoc.childNodes )),
+ preferredDoc.childNodes
+ );
+ // Support: Android<4.0
+ // Detect silently failing push.apply
+ arr[ preferredDoc.childNodes.length ].nodeType;
+} catch ( e ) {
+ push = { apply: arr.length ?
+
+ // Leverage slice if possible
+ function( target, els ) {
+ push_native.apply( target, slice.call(els) );
+ } :
+
+ // Support: IE<9
+ // Otherwise append directly
+ function( target, els ) {
+ var j = target.length,
+ i = 0;
+ // Can't trust NodeList.length
+ while ( (target[j++] = els[i++]) ) {}
+ target.length = j - 1;
+ }
+ };
+}
+
+function Sizzle( selector, context, results, seed ) {
+ var m, i, elem, nid, match, groups, newSelector,
+ newContext = context && context.ownerDocument,
+
+ // nodeType defaults to 9, since context defaults to document
+ nodeType = context ? context.nodeType : 9;
+
+ results = results || [];
+
+ // Return early from calls with invalid selector or context
+ if ( typeof selector !== "string" || !selector ||
+ nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {
+
+ return results;
+ }
+
+ // Try to shortcut find operations (as opposed to filters) in HTML documents
+ if ( !seed ) {
+
+ if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {
+ setDocument( context );
+ }
+ context = context || document;
+
+ if ( documentIsHTML ) {
+
+ // If the selector is sufficiently simple, try using a "get*By*" DOM method
+ // (excepting DocumentFragment context, where the methods don't exist)
+ if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) {
+
+ // ID selector
+ if ( (m = match[1]) ) {
+
+ // Document context
+ if ( nodeType === 9 ) {
+ if ( (elem = context.getElementById( m )) ) {
+
+ // Support: IE, Opera, Webkit
+ // TODO: identify versions
+ // getElementById can match elements by name instead of ID
+ if ( elem.id === m ) {
+ results.push( elem );
+ return results;
+ }
+ } else {
+ return results;
+ }
+
+ // Element context
+ } else {
+
+ // Support: IE, Opera, Webkit
+ // TODO: identify versions
+ // getElementById can match elements by name instead of ID
+ if ( newContext && (elem = newContext.getElementById( m )) &&
+ contains( context, elem ) &&
+ elem.id === m ) {
+
+ results.push( elem );
+ return results;
+ }
+ }
+
+ // Type selector
+ } else if ( match[2] ) {
+ push.apply( results, context.getElementsByTagName( selector ) );
+ return results;
+
+ // Class selector
+ } else if ( (m = match[3]) && support.getElementsByClassName &&
+ context.getElementsByClassName ) {
+
+ push.apply( results, context.getElementsByClassName( m ) );
+ return results;
+ }
+ }
+
+ // Take advantage of querySelectorAll
+ if ( support.qsa &&
+ !compilerCache[ selector + " " ] &&
+ (!rbuggyQSA || !rbuggyQSA.test( selector )) ) {
+
+ if ( nodeType !== 1 ) {
+ newContext = context;
+ newSelector = selector;
+
+ // qSA looks outside Element context, which is not what we want
+ // Thanks to Andrew Dupont for this workaround technique
+ // Support: IE <=8
+ // Exclude object elements
+ } else if ( context.nodeName.toLowerCase() !== "object" ) {
+
+ // Capture the context ID, setting it first if necessary
+ if ( (nid = context.getAttribute( "id" )) ) {
+ nid = nid.replace( rcssescape, fcssescape );
+ } else {
+ context.setAttribute( "id", (nid = expando) );
+ }
+
+ // Prefix every selector in the list
+ groups = tokenize( selector );
+ i = groups.length;
+ while ( i-- ) {
+ groups[i] = "#" + nid + " " + toSelector( groups[i] );
+ }
+ newSelector = groups.join( "," );
+
+ // Expand context for sibling selectors
+ newContext = rsibling.test( selector ) && testContext( context.parentNode ) ||
+ context;
+ }
+
+ if ( newSelector ) {
+ try {
+ push.apply( results,
+ newContext.querySelectorAll( newSelector )
+ );
+ return results;
+ } catch ( qsaError ) {
+ } finally {
+ if ( nid === expando ) {
+ context.removeAttribute( "id" );
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // All others
+ return select( selector.replace( rtrim, "$1" ), context, results, seed );
+}
+
+/**
+ * Create key-value caches of limited size
+ * @returns {function(string, object)} Returns the Object data after storing it on itself with
+ * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)
+ * deleting the oldest entry
+ */
+function createCache() {
+ var keys = [];
+
+ function cache( key, value ) {
+ // Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
+ if ( keys.push( key + " " ) > Expr.cacheLength ) {
+ // Only keep the most recent entries
+ delete cache[ keys.shift() ];
+ }
+ return (cache[ key + " " ] = value);
+ }
+ return cache;
+}
+
+/**
+ * Mark a function for special use by Sizzle
+ * @param {Function} fn The function to mark
+ */
+function markFunction( fn ) {
+ fn[ expando ] = true;
+ return fn;
+}
+
+/**
+ * Support testing using an element
+ * @param {Function} fn Passed the created element and returns a boolean result
+ */
+function assert( fn ) {
+ var el = document.createElement("fieldset");
+
+ try {
+ return !!fn( el );
+ } catch (e) {
+ return false;
+ } finally {
+ // Remove from its parent by default
+ if ( el.parentNode ) {
+ el.parentNode.removeChild( el );
+ }
+ // release memory in IE
+ el = null;
+ }
+}
+
+/**
+ * Adds the same handler for all of the specified attrs
+ * @param {String} attrs Pipe-separated list of attributes
+ * @param {Function} handler The method that will be applied
+ */
+function addHandle( attrs, handler ) {
+ var arr = attrs.split("|"),
+ i = arr.length;
+
+ while ( i-- ) {
+ Expr.attrHandle[ arr[i] ] = handler;
+ }
+}
+
+/**
+ * Checks document order of two siblings
+ * @param {Element} a
+ * @param {Element} b
+ * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b
+ */
+function siblingCheck( a, b ) {
+ var cur = b && a,
+ diff = cur && a.nodeType === 1 && b.nodeType === 1 &&
+ a.sourceIndex - b.sourceIndex;
+
+ // Use IE sourceIndex if available on both nodes
+ if ( diff ) {
+ return diff;
+ }
+
+ // Check if b follows a
+ if ( cur ) {
+ while ( (cur = cur.nextSibling) ) {
+ if ( cur === b ) {
+ return -1;
+ }
+ }
+ }
+
+ return a ? 1 : -1;
+}
+
+/**
+ * Returns a function to use in pseudos for input types
+ * @param {String} type
+ */
+function createInputPseudo( type ) {
+ return function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return name === "input" && elem.type === type;
+ };
+}
+
+/**
+ * Returns a function to use in pseudos for buttons
+ * @param {String} type
+ */
+function createButtonPseudo( type ) {
+ return function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return (name === "input" || name === "button") && elem.type === type;
+ };
+}
+
+/**
+ * Returns a function to use in pseudos for :enabled/:disabled
+ * @param {Boolean} disabled true for :disabled; false for :enabled
+ */
+function createDisabledPseudo( disabled ) {
+
+ // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable
+ return function( elem ) {
+
+ // Only certain elements can match :enabled or :disabled
+ // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled
+ // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled
+ if ( "form" in elem ) {
+
+ // Check for inherited disabledness on relevant non-disabled elements:
+ // * listed form-associated elements in a disabled fieldset
+ // https://html.spec.whatwg.org/multipage/forms.html#category-listed
+ // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled
+ // * option elements in a disabled optgroup
+ // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled
+ // All such elements have a "form" property.
+ if ( elem.parentNode && elem.disabled === false ) {
+
+ // Option elements defer to a parent optgroup if present
+ if ( "label" in elem ) {
+ if ( "label" in elem.parentNode ) {
+ return elem.parentNode.disabled === disabled;
+ } else {
+ return elem.disabled === disabled;
+ }
+ }
+
+ // Support: IE 6 - 11
+ // Use the isDisabled shortcut property to check for disabled fieldset ancestors
+ return elem.isDisabled === disabled ||
+
+ // Where there is no isDisabled, check manually
+ /* jshint -W018 */
+ elem.isDisabled !== !disabled &&
+ disabledAncestor( elem ) === disabled;
+ }
+
+ return elem.disabled === disabled;
+
+ // Try to winnow out elements that can't be disabled before trusting the disabled property.
+ // Some victims get caught in our net (label, legend, menu, track), but it shouldn't
+ // even exist on them, let alone have a boolean value.
+ } else if ( "label" in elem ) {
+ return elem.disabled === disabled;
+ }
+
+ // Remaining elements are neither :enabled nor :disabled
+ return false;
+ };
+}
+
+/**
+ * Returns a function to use in pseudos for positionals
+ * @param {Function} fn
+ */
+function createPositionalPseudo( fn ) {
+ return markFunction(function( argument ) {
+ argument = +argument;
+ return markFunction(function( seed, matches ) {
+ var j,
+ matchIndexes = fn( [], seed.length, argument ),
+ i = matchIndexes.length;
+
+ // Match elements found at the specified indexes
+ while ( i-- ) {
+ if ( seed[ (j = matchIndexes[i]) ] ) {
+ seed[j] = !(matches[j] = seed[j]);
+ }
+ }
+ });
+ });
+}
+
+/**
+ * Checks a node for validity as a Sizzle context
+ * @param {Element|Object=} context
+ * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value
+ */
+function testContext( context ) {
+ return context && typeof context.getElementsByTagName !== "undefined" && context;
+}
+
+// Expose support vars for convenience
+support = Sizzle.support = {};
+
+/**
+ * Detects XML nodes
+ * @param {Element|Object} elem An element or a document
+ * @returns {Boolean} True iff elem is a non-HTML XML node
+ */
+isXML = Sizzle.isXML = function( elem ) {
+ // documentElement is verified for cases where it doesn't yet exist
+ // (such as loading iframes in IE - #4833)
+ var documentElement = elem && (elem.ownerDocument || elem).documentElement;
+ return documentElement ? documentElement.nodeName !== "HTML" : false;
+};
+
+/**
+ * Sets document-related variables once based on the current document
+ * @param {Element|Object} [doc] An element or document object to use to set the document
+ * @returns {Object} Returns the current document
+ */
+setDocument = Sizzle.setDocument = function( node ) {
+ var hasCompare, subWindow,
+ doc = node ? node.ownerDocument || node : preferredDoc;
+
+ // Return early if doc is invalid or already selected
+ if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {
+ return document;
+ }
+
+ // Update global variables
+ document = doc;
+ docElem = document.documentElement;
+ documentIsHTML = !isXML( document );
+
+ // Support: IE 9-11, Edge
+ // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936)
+ if ( preferredDoc !== document &&
+ (subWindow = document.defaultView) && subWindow.top !== subWindow ) {
+
+ // Support: IE 11, Edge
+ if ( subWindow.addEventListener ) {
+ subWindow.addEventListener( "unload", unloadHandler, false );
+
+ // Support: IE 9 - 10 only
+ } else if ( subWindow.attachEvent ) {
+ subWindow.attachEvent( "onunload", unloadHandler );
+ }
+ }
+
+ /* Attributes
+ ---------------------------------------------------------------------- */
+
+ // Support: IE<8
+ // Verify that getAttribute really returns attributes and not properties
+ // (excepting IE8 booleans)
+ support.attributes = assert(function( el ) {
+ el.className = "i";
+ return !el.getAttribute("className");
+ });
+
+ /* getElement(s)By*
+ ---------------------------------------------------------------------- */
+
+ // Check if getElementsByTagName("*") returns only elements
+ support.getElementsByTagName = assert(function( el ) {
+ el.appendChild( document.createComment("") );
+ return !el.getElementsByTagName("*").length;
+ });
+
+ // Support: IE<9
+ support.getElementsByClassName = rnative.test( document.getElementsByClassName );
+
+ // Support: IE<10
+ // Check if getElementById returns elements by name
+ // The broken getElementById methods don't pick up programmatically-set names,
+ // so use a roundabout getElementsByName test
+ support.getById = assert(function( el ) {
+ docElem.appendChild( el ).id = expando;
+ return !document.getElementsByName || !document.getElementsByName( expando ).length;
+ });
+
+ // ID filter and find
+ if ( support.getById ) {
+ Expr.filter["ID"] = function( id ) {
+ var attrId = id.replace( runescape, funescape );
+ return function( elem ) {
+ return elem.getAttribute("id") === attrId;
+ };
+ };
+ Expr.find["ID"] = function( id, context ) {
+ if ( typeof context.getElementById !== "undefined" && documentIsHTML ) {
+ var elem = context.getElementById( id );
+ return elem ? [ elem ] : [];
+ }
+ };
+ } else {
+ Expr.filter["ID"] = function( id ) {
+ var attrId = id.replace( runescape, funescape );
+ return function( elem ) {
+ var node = typeof elem.getAttributeNode !== "undefined" &&
+ elem.getAttributeNode("id");
+ return node && node.value === attrId;
+ };
+ };
+
+ // Support: IE 6 - 7 only
+ // getElementById is not reliable as a find shortcut
+ Expr.find["ID"] = function( id, context ) {
+ if ( typeof context.getElementById !== "undefined" && documentIsHTML ) {
+ var node, i, elems,
+ elem = context.getElementById( id );
+
+ if ( elem ) {
+
+ // Verify the id attribute
+ node = elem.getAttributeNode("id");
+ if ( node && node.value === id ) {
+ return [ elem ];
+ }
+
+ // Fall back on getElementsByName
+ elems = context.getElementsByName( id );
+ i = 0;
+ while ( (elem = elems[i++]) ) {
+ node = elem.getAttributeNode("id");
+ if ( node && node.value === id ) {
+ return [ elem ];
+ }
+ }
+ }
+
+ return [];
+ }
+ };
+ }
+
+ // Tag
+ Expr.find["TAG"] = support.getElementsByTagName ?
+ function( tag, context ) {
+ if ( typeof context.getElementsByTagName !== "undefined" ) {
+ return context.getElementsByTagName( tag );
+
+ // DocumentFragment nodes don't have gEBTN
+ } else if ( support.qsa ) {
+ return context.querySelectorAll( tag );
+ }
+ } :
+
+ function( tag, context ) {
+ var elem,
+ tmp = [],
+ i = 0,
+ // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too
+ results = context.getElementsByTagName( tag );
+
+ // Filter out possible comments
+ if ( tag === "*" ) {
+ while ( (elem = results[i++]) ) {
+ if ( elem.nodeType === 1 ) {
+ tmp.push( elem );
+ }
+ }
+
+ return tmp;
+ }
+ return results;
+ };
+
+ // Class
+ Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) {
+ if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) {
+ return context.getElementsByClassName( className );
+ }
+ };
+
+ /* QSA/matchesSelector
+ ---------------------------------------------------------------------- */
+
+ // QSA and matchesSelector support
+
+ // matchesSelector(:active) reports false when true (IE9/Opera 11.5)
+ rbuggyMatches = [];
+
+ // qSa(:focus) reports false when true (Chrome 21)
+ // We allow this because of a bug in IE8/9 that throws an error
+ // whenever `document.activeElement` is accessed on an iframe
+ // So, we allow :focus to pass through QSA all the time to avoid the IE error
+ // See https://bugs.jquery.com/ticket/13378
+ rbuggyQSA = [];
+
+ if ( (support.qsa = rnative.test( document.querySelectorAll )) ) {
+ // Build QSA regex
+ // Regex strategy adopted from Diego Perini
+ assert(function( el ) {
+ // Select is set to empty string on purpose
+ // This is to test IE's treatment of not explicitly
+ // setting a boolean content attribute,
+ // since its presence should be enough
+ // https://bugs.jquery.com/ticket/12359
+ docElem.appendChild( el ).innerHTML = "" +
+ "";
+
+ // Support: IE8, Opera 11-12.16
+ // Nothing should be selected when empty strings follow ^= or $= or *=
+ // The test attribute must be unknown in Opera but "safe" for WinRT
+ // https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section
+ if ( el.querySelectorAll("[msallowcapture^='']").length ) {
+ rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" );
+ }
+
+ // Support: IE8
+ // Boolean attributes and "value" are not treated correctly
+ if ( !el.querySelectorAll("[selected]").length ) {
+ rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" );
+ }
+
+ // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+
+ if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) {
+ rbuggyQSA.push("~=");
+ }
+
+ // Webkit/Opera - :checked should return selected option elements
+ // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+ // IE8 throws error here and will not see later tests
+ if ( !el.querySelectorAll(":checked").length ) {
+ rbuggyQSA.push(":checked");
+ }
+
+ // Support: Safari 8+, iOS 8+
+ // https://bugs.webkit.org/show_bug.cgi?id=136851
+ // In-page `selector#id sibling-combinator selector` fails
+ if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) {
+ rbuggyQSA.push(".#.+[+~]");
+ }
+ });
+
+ assert(function( el ) {
+ el.innerHTML = "" +
+ "";
+
+ // Support: Windows 8 Native Apps
+ // The type and name attributes are restricted during .innerHTML assignment
+ var input = document.createElement("input");
+ input.setAttribute( "type", "hidden" );
+ el.appendChild( input ).setAttribute( "name", "D" );
+
+ // Support: IE8
+ // Enforce case-sensitivity of name attribute
+ if ( el.querySelectorAll("[name=d]").length ) {
+ rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" );
+ }
+
+ // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
+ // IE8 throws error here and will not see later tests
+ if ( el.querySelectorAll(":enabled").length !== 2 ) {
+ rbuggyQSA.push( ":enabled", ":disabled" );
+ }
+
+ // Support: IE9-11+
+ // IE's :disabled selector does not pick up the children of disabled fieldsets
+ docElem.appendChild( el ).disabled = true;
+ if ( el.querySelectorAll(":disabled").length !== 2 ) {
+ rbuggyQSA.push( ":enabled", ":disabled" );
+ }
+
+ // Opera 10-11 does not throw on post-comma invalid pseudos
+ el.querySelectorAll("*,:x");
+ rbuggyQSA.push(",.*:");
+ });
+ }
+
+ if ( (support.matchesSelector = rnative.test( (matches = docElem.matches ||
+ docElem.webkitMatchesSelector ||
+ docElem.mozMatchesSelector ||
+ docElem.oMatchesSelector ||
+ docElem.msMatchesSelector) )) ) {
+
+ assert(function( el ) {
+ // Check to see if it's possible to do matchesSelector
+ // on a disconnected node (IE 9)
+ support.disconnectedMatch = matches.call( el, "*" );
+
+ // This should fail with an exception
+ // Gecko does not error, returns false instead
+ matches.call( el, "[s!='']:x" );
+ rbuggyMatches.push( "!=", pseudos );
+ });
+ }
+
+ rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") );
+ rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") );
+
+ /* Contains
+ ---------------------------------------------------------------------- */
+ hasCompare = rnative.test( docElem.compareDocumentPosition );
+
+ // Element contains another
+ // Purposefully self-exclusive
+ // As in, an element does not contain itself
+ contains = hasCompare || rnative.test( docElem.contains ) ?
+ function( a, b ) {
+ var adown = a.nodeType === 9 ? a.documentElement : a,
+ bup = b && b.parentNode;
+ return a === bup || !!( bup && bup.nodeType === 1 && (
+ adown.contains ?
+ adown.contains( bup ) :
+ a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
+ ));
+ } :
+ function( a, b ) {
+ if ( b ) {
+ while ( (b = b.parentNode) ) {
+ if ( b === a ) {
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+
+ /* Sorting
+ ---------------------------------------------------------------------- */
+
+ // Document order sorting
+ sortOrder = hasCompare ?
+ function( a, b ) {
+
+ // Flag for duplicate removal
+ if ( a === b ) {
+ hasDuplicate = true;
+ return 0;
+ }
+
+ // Sort on method existence if only one input has compareDocumentPosition
+ var compare = !a.compareDocumentPosition - !b.compareDocumentPosition;
+ if ( compare ) {
+ return compare;
+ }
+
+ // Calculate position if both inputs belong to the same document
+ compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ?
+ a.compareDocumentPosition( b ) :
+
+ // Otherwise we know they are disconnected
+ 1;
+
+ // Disconnected nodes
+ if ( compare & 1 ||
+ (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {
+
+ // Choose the first element that is related to our preferred document
+ if ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) {
+ return -1;
+ }
+ if ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) {
+ return 1;
+ }
+
+ // Maintain original order
+ return sortInput ?
+ ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
+ 0;
+ }
+
+ return compare & 4 ? -1 : 1;
+ } :
+ function( a, b ) {
+ // Exit early if the nodes are identical
+ if ( a === b ) {
+ hasDuplicate = true;
+ return 0;
+ }
+
+ var cur,
+ i = 0,
+ aup = a.parentNode,
+ bup = b.parentNode,
+ ap = [ a ],
+ bp = [ b ];
+
+ // Parentless nodes are either documents or disconnected
+ if ( !aup || !bup ) {
+ return a === document ? -1 :
+ b === document ? 1 :
+ aup ? -1 :
+ bup ? 1 :
+ sortInput ?
+ ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
+ 0;
+
+ // If the nodes are siblings, we can do a quick check
+ } else if ( aup === bup ) {
+ return siblingCheck( a, b );
+ }
+
+ // Otherwise we need full lists of their ancestors for comparison
+ cur = a;
+ while ( (cur = cur.parentNode) ) {
+ ap.unshift( cur );
+ }
+ cur = b;
+ while ( (cur = cur.parentNode) ) {
+ bp.unshift( cur );
+ }
+
+ // Walk down the tree looking for a discrepancy
+ while ( ap[i] === bp[i] ) {
+ i++;
+ }
+
+ return i ?
+ // Do a sibling check if the nodes have a common ancestor
+ siblingCheck( ap[i], bp[i] ) :
+
+ // Otherwise nodes in our document sort first
+ ap[i] === preferredDoc ? -1 :
+ bp[i] === preferredDoc ? 1 :
+ 0;
+ };
+
+ return document;
+};
+
+Sizzle.matches = function( expr, elements ) {
+ return Sizzle( expr, null, null, elements );
+};
+
+Sizzle.matchesSelector = function( elem, expr ) {
+ // Set document vars if needed
+ if ( ( elem.ownerDocument || elem ) !== document ) {
+ setDocument( elem );
+ }
+
+ // Make sure that attribute selectors are quoted
+ expr = expr.replace( rattributeQuotes, "='$1']" );
+
+ if ( support.matchesSelector && documentIsHTML &&
+ !compilerCache[ expr + " " ] &&
+ ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&
+ ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) {
+
+ try {
+ var ret = matches.call( elem, expr );
+
+ // IE 9's matchesSelector returns false on disconnected nodes
+ if ( ret || support.disconnectedMatch ||
+ // As well, disconnected nodes are said to be in a document
+ // fragment in IE 9
+ elem.document && elem.document.nodeType !== 11 ) {
+ return ret;
+ }
+ } catch (e) {}
+ }
+
+ return Sizzle( expr, document, null, [ elem ] ).length > 0;
+};
+
+Sizzle.contains = function( context, elem ) {
+ // Set document vars if needed
+ if ( ( context.ownerDocument || context ) !== document ) {
+ setDocument( context );
+ }
+ return contains( context, elem );
+};
+
+Sizzle.attr = function( elem, name ) {
+ // Set document vars if needed
+ if ( ( elem.ownerDocument || elem ) !== document ) {
+ setDocument( elem );
+ }
+
+ var fn = Expr.attrHandle[ name.toLowerCase() ],
+ // Don't get fooled by Object.prototype properties (jQuery #13807)
+ val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?
+ fn( elem, name, !documentIsHTML ) :
+ undefined;
+
+ return val !== undefined ?
+ val :
+ support.attributes || !documentIsHTML ?
+ elem.getAttribute( name ) :
+ (val = elem.getAttributeNode(name)) && val.specified ?
+ val.value :
+ null;
+};
+
+Sizzle.escape = function( sel ) {
+ return (sel + "").replace( rcssescape, fcssescape );
+};
+
+Sizzle.error = function( msg ) {
+ throw new Error( "Syntax error, unrecognized expression: " + msg );
+};
+
+/**
+ * Document sorting and removing duplicates
+ * @param {ArrayLike} results
+ */
+Sizzle.uniqueSort = function( results ) {
+ var elem,
+ duplicates = [],
+ j = 0,
+ i = 0;
+
+ // Unless we *know* we can detect duplicates, assume their presence
+ hasDuplicate = !support.detectDuplicates;
+ sortInput = !support.sortStable && results.slice( 0 );
+ results.sort( sortOrder );
+
+ if ( hasDuplicate ) {
+ while ( (elem = results[i++]) ) {
+ if ( elem === results[ i ] ) {
+ j = duplicates.push( i );
+ }
+ }
+ while ( j-- ) {
+ results.splice( duplicates[ j ], 1 );
+ }
+ }
+
+ // Clear input after sorting to release objects
+ // See https://github.com/jquery/sizzle/pull/225
+ sortInput = null;
+
+ return results;
+};
+
+/**
+ * Utility function for retrieving the text value of an array of DOM nodes
+ * @param {Array|Element} elem
+ */
+getText = Sizzle.getText = function( elem ) {
+ var node,
+ ret = "",
+ i = 0,
+ nodeType = elem.nodeType;
+
+ if ( !nodeType ) {
+ // If no nodeType, this is expected to be an array
+ while ( (node = elem[i++]) ) {
+ // Do not traverse comment nodes
+ ret += getText( node );
+ }
+ } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
+ // Use textContent for elements
+ // innerText usage removed for consistency of new lines (jQuery #11153)
+ if ( typeof elem.textContent === "string" ) {
+ return elem.textContent;
+ } else {
+ // Traverse its children
+ for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+ ret += getText( elem );
+ }
+ }
+ } else if ( nodeType === 3 || nodeType === 4 ) {
+ return elem.nodeValue;
+ }
+ // Do not include comment or processing instruction nodes
+
+ return ret;
+};
+
+Expr = Sizzle.selectors = {
+
+ // Can be adjusted by the user
+ cacheLength: 50,
+
+ createPseudo: markFunction,
+
+ match: matchExpr,
+
+ attrHandle: {},
+
+ find: {},
+
+ relative: {
+ ">": { dir: "parentNode", first: true },
+ " ": { dir: "parentNode" },
+ "+": { dir: "previousSibling", first: true },
+ "~": { dir: "previousSibling" }
+ },
+
+ preFilter: {
+ "ATTR": function( match ) {
+ match[1] = match[1].replace( runescape, funescape );
+
+ // Move the given value to match[3] whether quoted or unquoted
+ match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape );
+
+ if ( match[2] === "~=" ) {
+ match[3] = " " + match[3] + " ";
+ }
+
+ return match.slice( 0, 4 );
+ },
+
+ "CHILD": function( match ) {
+ /* matches from matchExpr["CHILD"]
+ 1 type (only|nth|...)
+ 2 what (child|of-type)
+ 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...)
+ 4 xn-component of xn+y argument ([+-]?\d*n|)
+ 5 sign of xn-component
+ 6 x of xn-component
+ 7 sign of y-component
+ 8 y of y-component
+ */
+ match[1] = match[1].toLowerCase();
+
+ if ( match[1].slice( 0, 3 ) === "nth" ) {
+ // nth-* requires argument
+ if ( !match[3] ) {
+ Sizzle.error( match[0] );
+ }
+
+ // numeric x and y parameters for Expr.filter.CHILD
+ // remember that false/true cast respectively to 0/1
+ match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) );
+ match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" );
+
+ // other types prohibit arguments
+ } else if ( match[3] ) {
+ Sizzle.error( match[0] );
+ }
+
+ return match;
+ },
+
+ "PSEUDO": function( match ) {
+ var excess,
+ unquoted = !match[6] && match[2];
+
+ if ( matchExpr["CHILD"].test( match[0] ) ) {
+ return null;
+ }
+
+ // Accept quoted arguments as-is
+ if ( match[3] ) {
+ match[2] = match[4] || match[5] || "";
+
+ // Strip excess characters from unquoted arguments
+ } else if ( unquoted && rpseudo.test( unquoted ) &&
+ // Get excess from tokenize (recursively)
+ (excess = tokenize( unquoted, true )) &&
+ // advance to the next closing parenthesis
+ (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) {
+
+ // excess is a negative index
+ match[0] = match[0].slice( 0, excess );
+ match[2] = unquoted.slice( 0, excess );
+ }
+
+ // Return only captures needed by the pseudo filter method (type and argument)
+ return match.slice( 0, 3 );
+ }
+ },
+
+ filter: {
+
+ "TAG": function( nodeNameSelector ) {
+ var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();
+ return nodeNameSelector === "*" ?
+ function() { return true; } :
+ function( elem ) {
+ return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
+ };
+ },
+
+ "CLASS": function( className ) {
+ var pattern = classCache[ className + " " ];
+
+ return pattern ||
+ (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) &&
+ classCache( className, function( elem ) {
+ return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" );
+ });
+ },
+
+ "ATTR": function( name, operator, check ) {
+ return function( elem ) {
+ var result = Sizzle.attr( elem, name );
+
+ if ( result == null ) {
+ return operator === "!=";
+ }
+ if ( !operator ) {
+ return true;
+ }
+
+ result += "";
+
+ return operator === "=" ? result === check :
+ operator === "!=" ? result !== check :
+ operator === "^=" ? check && result.indexOf( check ) === 0 :
+ operator === "*=" ? check && result.indexOf( check ) > -1 :
+ operator === "$=" ? check && result.slice( -check.length ) === check :
+ operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 :
+ operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" :
+ false;
+ };
+ },
+
+ "CHILD": function( type, what, argument, first, last ) {
+ var simple = type.slice( 0, 3 ) !== "nth",
+ forward = type.slice( -4 ) !== "last",
+ ofType = what === "of-type";
+
+ return first === 1 && last === 0 ?
+
+ // Shortcut for :nth-*(n)
+ function( elem ) {
+ return !!elem.parentNode;
+ } :
+
+ function( elem, context, xml ) {
+ var cache, uniqueCache, outerCache, node, nodeIndex, start,
+ dir = simple !== forward ? "nextSibling" : "previousSibling",
+ parent = elem.parentNode,
+ name = ofType && elem.nodeName.toLowerCase(),
+ useCache = !xml && !ofType,
+ diff = false;
+
+ if ( parent ) {
+
+ // :(first|last|only)-(child|of-type)
+ if ( simple ) {
+ while ( dir ) {
+ node = elem;
+ while ( (node = node[ dir ]) ) {
+ if ( ofType ?
+ node.nodeName.toLowerCase() === name :
+ node.nodeType === 1 ) {
+
+ return false;
+ }
+ }
+ // Reverse direction for :only-* (if we haven't yet done so)
+ start = dir = type === "only" && !start && "nextSibling";
+ }
+ return true;
+ }
+
+ start = [ forward ? parent.firstChild : parent.lastChild ];
+
+ // non-xml :nth-child(...) stores cache data on `parent`
+ if ( forward && useCache ) {
+
+ // Seek `elem` from a previously-cached index
+
+ // ...in a gzip-friendly way
+ node = parent;
+ outerCache = node[ expando ] || (node[ expando ] = {});
+
+ // Support: IE <9 only
+ // Defend against cloned attroperties (jQuery gh-1709)
+ uniqueCache = outerCache[ node.uniqueID ] ||
+ (outerCache[ node.uniqueID ] = {});
+
+ cache = uniqueCache[ type ] || [];
+ nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];
+ diff = nodeIndex && cache[ 2 ];
+ node = nodeIndex && parent.childNodes[ nodeIndex ];
+
+ while ( (node = ++nodeIndex && node && node[ dir ] ||
+
+ // Fallback to seeking `elem` from the start
+ (diff = nodeIndex = 0) || start.pop()) ) {
+
+ // When found, cache indexes on `parent` and break
+ if ( node.nodeType === 1 && ++diff && node === elem ) {
+ uniqueCache[ type ] = [ dirruns, nodeIndex, diff ];
+ break;
+ }
+ }
+
+ } else {
+ // Use previously-cached element index if available
+ if ( useCache ) {
+ // ...in a gzip-friendly way
+ node = elem;
+ outerCache = node[ expando ] || (node[ expando ] = {});
+
+ // Support: IE <9 only
+ // Defend against cloned attroperties (jQuery gh-1709)
+ uniqueCache = outerCache[ node.uniqueID ] ||
+ (outerCache[ node.uniqueID ] = {});
+
+ cache = uniqueCache[ type ] || [];
+ nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];
+ diff = nodeIndex;
+ }
+
+ // xml :nth-child(...)
+ // or :nth-last-child(...) or :nth(-last)?-of-type(...)
+ if ( diff === false ) {
+ // Use the same loop as above to seek `elem` from the start
+ while ( (node = ++nodeIndex && node && node[ dir ] ||
+ (diff = nodeIndex = 0) || start.pop()) ) {
+
+ if ( ( ofType ?
+ node.nodeName.toLowerCase() === name :
+ node.nodeType === 1 ) &&
+ ++diff ) {
+
+ // Cache the index of each encountered element
+ if ( useCache ) {
+ outerCache = node[ expando ] || (node[ expando ] = {});
+
+ // Support: IE <9 only
+ // Defend against cloned attroperties (jQuery gh-1709)
+ uniqueCache = outerCache[ node.uniqueID ] ||
+ (outerCache[ node.uniqueID ] = {});
+
+ uniqueCache[ type ] = [ dirruns, diff ];
+ }
+
+ if ( node === elem ) {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // Incorporate the offset, then check against cycle size
+ diff -= last;
+ return diff === first || ( diff % first === 0 && diff / first >= 0 );
+ }
+ };
+ },
+
+ "PSEUDO": function( pseudo, argument ) {
+ // pseudo-class names are case-insensitive
+ // http://www.w3.org/TR/selectors/#pseudo-classes
+ // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
+ // Remember that setFilters inherits from pseudos
+ var args,
+ fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||
+ Sizzle.error( "unsupported pseudo: " + pseudo );
+
+ // The user may use createPseudo to indicate that
+ // arguments are needed to create the filter function
+ // just as Sizzle does
+ if ( fn[ expando ] ) {
+ return fn( argument );
+ }
+
+ // But maintain support for old signatures
+ if ( fn.length > 1 ) {
+ args = [ pseudo, pseudo, "", argument ];
+ return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?
+ markFunction(function( seed, matches ) {
+ var idx,
+ matched = fn( seed, argument ),
+ i = matched.length;
+ while ( i-- ) {
+ idx = indexOf( seed, matched[i] );
+ seed[ idx ] = !( matches[ idx ] = matched[i] );
+ }
+ }) :
+ function( elem ) {
+ return fn( elem, 0, args );
+ };
+ }
+
+ return fn;
+ }
+ },
+
+ pseudos: {
+ // Potentially complex pseudos
+ "not": markFunction(function( selector ) {
+ // Trim the selector passed to compile
+ // to avoid treating leading and trailing
+ // spaces as combinators
+ var input = [],
+ results = [],
+ matcher = compile( selector.replace( rtrim, "$1" ) );
+
+ return matcher[ expando ] ?
+ markFunction(function( seed, matches, context, xml ) {
+ var elem,
+ unmatched = matcher( seed, null, xml, [] ),
+ i = seed.length;
+
+ // Match elements unmatched by `matcher`
+ while ( i-- ) {
+ if ( (elem = unmatched[i]) ) {
+ seed[i] = !(matches[i] = elem);
+ }
+ }
+ }) :
+ function( elem, context, xml ) {
+ input[0] = elem;
+ matcher( input, null, xml, results );
+ // Don't keep the element (issue #299)
+ input[0] = null;
+ return !results.pop();
+ };
+ }),
+
+ "has": markFunction(function( selector ) {
+ return function( elem ) {
+ return Sizzle( selector, elem ).length > 0;
+ };
+ }),
+
+ "contains": markFunction(function( text ) {
+ text = text.replace( runescape, funescape );
+ return function( elem ) {
+ return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;
+ };
+ }),
+
+ // "Whether an element is represented by a :lang() selector
+ // is based solely on the element's language value
+ // being equal to the identifier C,
+ // or beginning with the identifier C immediately followed by "-".
+ // The matching of C against the element's language value is performed case-insensitively.
+ // The identifier C does not have to be a valid language name."
+ // http://www.w3.org/TR/selectors/#lang-pseudo
+ "lang": markFunction( function( lang ) {
+ // lang value must be a valid identifier
+ if ( !ridentifier.test(lang || "") ) {
+ Sizzle.error( "unsupported lang: " + lang );
+ }
+ lang = lang.replace( runescape, funescape ).toLowerCase();
+ return function( elem ) {
+ var elemLang;
+ do {
+ if ( (elemLang = documentIsHTML ?
+ elem.lang :
+ elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) {
+
+ elemLang = elemLang.toLowerCase();
+ return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0;
+ }
+ } while ( (elem = elem.parentNode) && elem.nodeType === 1 );
+ return false;
+ };
+ }),
+
+ // Miscellaneous
+ "target": function( elem ) {
+ var hash = window.location && window.location.hash;
+ return hash && hash.slice( 1 ) === elem.id;
+ },
+
+ "root": function( elem ) {
+ return elem === docElem;
+ },
+
+ "focus": function( elem ) {
+ return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);
+ },
+
+ // Boolean properties
+ "enabled": createDisabledPseudo( false ),
+ "disabled": createDisabledPseudo( true ),
+
+ "checked": function( elem ) {
+ // In CSS3, :checked should return both checked and selected elements
+ // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+ var nodeName = elem.nodeName.toLowerCase();
+ return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected);
+ },
+
+ "selected": function( elem ) {
+ // Accessing this property makes selected-by-default
+ // options in Safari work properly
+ if ( elem.parentNode ) {
+ elem.parentNode.selectedIndex;
+ }
+
+ return elem.selected === true;
+ },
+
+ // Contents
+ "empty": function( elem ) {
+ // http://www.w3.org/TR/selectors/#empty-pseudo
+ // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),
+ // but not by others (comment: 8; processing instruction: 7; etc.)
+ // nodeType < 6 works because attributes (2) do not appear as children
+ for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+ if ( elem.nodeType < 6 ) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ "parent": function( elem ) {
+ return !Expr.pseudos["empty"]( elem );
+ },
+
+ // Element/input types
+ "header": function( elem ) {
+ return rheader.test( elem.nodeName );
+ },
+
+ "input": function( elem ) {
+ return rinputs.test( elem.nodeName );
+ },
+
+ "button": function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return name === "input" && elem.type === "button" || name === "button";
+ },
+
+ "text": function( elem ) {
+ var attr;
+ return elem.nodeName.toLowerCase() === "input" &&
+ elem.type === "text" &&
+
+ // Support: IE<8
+ // New HTML5 attribute values (e.g., "search") appear with elem.type === "text"
+ ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" );
+ },
+
+ // Position-in-collection
+ "first": createPositionalPseudo(function() {
+ return [ 0 ];
+ }),
+
+ "last": createPositionalPseudo(function( matchIndexes, length ) {
+ return [ length - 1 ];
+ }),
+
+ "eq": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ return [ argument < 0 ? argument + length : argument ];
+ }),
+
+ "even": createPositionalPseudo(function( matchIndexes, length ) {
+ var i = 0;
+ for ( ; i < length; i += 2 ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ }),
+
+ "odd": createPositionalPseudo(function( matchIndexes, length ) {
+ var i = 1;
+ for ( ; i < length; i += 2 ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ }),
+
+ "lt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ var i = argument < 0 ? argument + length : argument;
+ for ( ; --i >= 0; ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ }),
+
+ "gt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ var i = argument < 0 ? argument + length : argument;
+ for ( ; ++i < length; ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ })
+ }
+};
+
+Expr.pseudos["nth"] = Expr.pseudos["eq"];
+
+// Add button/input type pseudos
+for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {
+ Expr.pseudos[ i ] = createInputPseudo( i );
+}
+for ( i in { submit: true, reset: true } ) {
+ Expr.pseudos[ i ] = createButtonPseudo( i );
+}
+
+// Easy API for creating new setFilters
+function setFilters() {}
+setFilters.prototype = Expr.filters = Expr.pseudos;
+Expr.setFilters = new setFilters();
+
+tokenize = Sizzle.tokenize = function( selector, parseOnly ) {
+ var matched, match, tokens, type,
+ soFar, groups, preFilters,
+ cached = tokenCache[ selector + " " ];
+
+ if ( cached ) {
+ return parseOnly ? 0 : cached.slice( 0 );
+ }
+
+ soFar = selector;
+ groups = [];
+ preFilters = Expr.preFilter;
+
+ while ( soFar ) {
+
+ // Comma and first run
+ if ( !matched || (match = rcomma.exec( soFar )) ) {
+ if ( match ) {
+ // Don't consume trailing commas as valid
+ soFar = soFar.slice( match[0].length ) || soFar;
+ }
+ groups.push( (tokens = []) );
+ }
+
+ matched = false;
+
+ // Combinators
+ if ( (match = rcombinators.exec( soFar )) ) {
+ matched = match.shift();
+ tokens.push({
+ value: matched,
+ // Cast descendant combinators to space
+ type: match[0].replace( rtrim, " " )
+ });
+ soFar = soFar.slice( matched.length );
+ }
+
+ // Filters
+ for ( type in Expr.filter ) {
+ if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||
+ (match = preFilters[ type ]( match ))) ) {
+ matched = match.shift();
+ tokens.push({
+ value: matched,
+ type: type,
+ matches: match
+ });
+ soFar = soFar.slice( matched.length );
+ }
+ }
+
+ if ( !matched ) {
+ break;
+ }
+ }
+
+ // Return the length of the invalid excess
+ // if we're just parsing
+ // Otherwise, throw an error or return tokens
+ return parseOnly ?
+ soFar.length :
+ soFar ?
+ Sizzle.error( selector ) :
+ // Cache the tokens
+ tokenCache( selector, groups ).slice( 0 );
+};
+
+function toSelector( tokens ) {
+ var i = 0,
+ len = tokens.length,
+ selector = "";
+ for ( ; i < len; i++ ) {
+ selector += tokens[i].value;
+ }
+ return selector;
+}
+
+function addCombinator( matcher, combinator, base ) {
+ var dir = combinator.dir,
+ skip = combinator.next,
+ key = skip || dir,
+ checkNonElements = base && key === "parentNode",
+ doneName = done++;
+
+ return combinator.first ?
+ // Check against closest ancestor/preceding element
+ function( elem, context, xml ) {
+ while ( (elem = elem[ dir ]) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ return matcher( elem, context, xml );
+ }
+ }
+ return false;
+ } :
+
+ // Check against all ancestor/preceding elements
+ function( elem, context, xml ) {
+ var oldCache, uniqueCache, outerCache,
+ newCache = [ dirruns, doneName ];
+
+ // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching
+ if ( xml ) {
+ while ( (elem = elem[ dir ]) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ if ( matcher( elem, context, xml ) ) {
+ return true;
+ }
+ }
+ }
+ } else {
+ while ( (elem = elem[ dir ]) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ outerCache = elem[ expando ] || (elem[ expando ] = {});
+
+ // Support: IE <9 only
+ // Defend against cloned attroperties (jQuery gh-1709)
+ uniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {});
+
+ if ( skip && skip === elem.nodeName.toLowerCase() ) {
+ elem = elem[ dir ] || elem;
+ } else if ( (oldCache = uniqueCache[ key ]) &&
+ oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {
+
+ // Assign to newCache so results back-propagate to previous elements
+ return (newCache[ 2 ] = oldCache[ 2 ]);
+ } else {
+ // Reuse newcache so results back-propagate to previous elements
+ uniqueCache[ key ] = newCache;
+
+ // A match means we're done; a fail means we have to keep checking
+ if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ };
+}
+
+function elementMatcher( matchers ) {
+ return matchers.length > 1 ?
+ function( elem, context, xml ) {
+ var i = matchers.length;
+ while ( i-- ) {
+ if ( !matchers[i]( elem, context, xml ) ) {
+ return false;
+ }
+ }
+ return true;
+ } :
+ matchers[0];
+}
+
+function multipleContexts( selector, contexts, results ) {
+ var i = 0,
+ len = contexts.length;
+ for ( ; i < len; i++ ) {
+ Sizzle( selector, contexts[i], results );
+ }
+ return results;
+}
+
+function condense( unmatched, map, filter, context, xml ) {
+ var elem,
+ newUnmatched = [],
+ i = 0,
+ len = unmatched.length,
+ mapped = map != null;
+
+ for ( ; i < len; i++ ) {
+ if ( (elem = unmatched[i]) ) {
+ if ( !filter || filter( elem, context, xml ) ) {
+ newUnmatched.push( elem );
+ if ( mapped ) {
+ map.push( i );
+ }
+ }
+ }
+ }
+
+ return newUnmatched;
+}
+
+function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {
+ if ( postFilter && !postFilter[ expando ] ) {
+ postFilter = setMatcher( postFilter );
+ }
+ if ( postFinder && !postFinder[ expando ] ) {
+ postFinder = setMatcher( postFinder, postSelector );
+ }
+ return markFunction(function( seed, results, context, xml ) {
+ var temp, i, elem,
+ preMap = [],
+ postMap = [],
+ preexisting = results.length,
+
+ // Get initial elements from seed or context
+ elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ),
+
+ // Prefilter to get matcher input, preserving a map for seed-results synchronization
+ matcherIn = preFilter && ( seed || !selector ) ?
+ condense( elems, preMap, preFilter, context, xml ) :
+ elems,
+
+ matcherOut = matcher ?
+ // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
+ postFinder || ( seed ? preFilter : preexisting || postFilter ) ?
+
+ // ...intermediate processing is necessary
+ [] :
+
+ // ...otherwise use results directly
+ results :
+ matcherIn;
+
+ // Find primary matches
+ if ( matcher ) {
+ matcher( matcherIn, matcherOut, context, xml );
+ }
+
+ // Apply postFilter
+ if ( postFilter ) {
+ temp = condense( matcherOut, postMap );
+ postFilter( temp, [], context, xml );
+
+ // Un-match failing elements by moving them back to matcherIn
+ i = temp.length;
+ while ( i-- ) {
+ if ( (elem = temp[i]) ) {
+ matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);
+ }
+ }
+ }
+
+ if ( seed ) {
+ if ( postFinder || preFilter ) {
+ if ( postFinder ) {
+ // Get the final matcherOut by condensing this intermediate into postFinder contexts
+ temp = [];
+ i = matcherOut.length;
+ while ( i-- ) {
+ if ( (elem = matcherOut[i]) ) {
+ // Restore matcherIn since elem is not yet a final match
+ temp.push( (matcherIn[i] = elem) );
+ }
+ }
+ postFinder( null, (matcherOut = []), temp, xml );
+ }
+
+ // Move matched elements from seed to results to keep them synchronized
+ i = matcherOut.length;
+ while ( i-- ) {
+ if ( (elem = matcherOut[i]) &&
+ (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) {
+
+ seed[temp] = !(results[temp] = elem);
+ }
+ }
+ }
+
+ // Add elements to results, through postFinder if defined
+ } else {
+ matcherOut = condense(
+ matcherOut === results ?
+ matcherOut.splice( preexisting, matcherOut.length ) :
+ matcherOut
+ );
+ if ( postFinder ) {
+ postFinder( null, results, matcherOut, xml );
+ } else {
+ push.apply( results, matcherOut );
+ }
+ }
+ });
+}
+
+function matcherFromTokens( tokens ) {
+ var checkContext, matcher, j,
+ len = tokens.length,
+ leadingRelative = Expr.relative[ tokens[0].type ],
+ implicitRelative = leadingRelative || Expr.relative[" "],
+ i = leadingRelative ? 1 : 0,
+
+ // The foundational matcher ensures that elements are reachable from top-level context(s)
+ matchContext = addCombinator( function( elem ) {
+ return elem === checkContext;
+ }, implicitRelative, true ),
+ matchAnyContext = addCombinator( function( elem ) {
+ return indexOf( checkContext, elem ) > -1;
+ }, implicitRelative, true ),
+ matchers = [ function( elem, context, xml ) {
+ var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
+ (checkContext = context).nodeType ?
+ matchContext( elem, context, xml ) :
+ matchAnyContext( elem, context, xml ) );
+ // Avoid hanging onto element (issue #299)
+ checkContext = null;
+ return ret;
+ } ];
+
+ for ( ; i < len; i++ ) {
+ if ( (matcher = Expr.relative[ tokens[i].type ]) ) {
+ matchers = [ addCombinator(elementMatcher( matchers ), matcher) ];
+ } else {
+ matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );
+
+ // Return special upon seeing a positional matcher
+ if ( matcher[ expando ] ) {
+ // Find the next relative operator (if any) for proper handling
+ j = ++i;
+ for ( ; j < len; j++ ) {
+ if ( Expr.relative[ tokens[j].type ] ) {
+ break;
+ }
+ }
+ return setMatcher(
+ i > 1 && elementMatcher( matchers ),
+ i > 1 && toSelector(
+ // If the preceding token was a descendant combinator, insert an implicit any-element `*`
+ tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" })
+ ).replace( rtrim, "$1" ),
+ matcher,
+ i < j && matcherFromTokens( tokens.slice( i, j ) ),
+ j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),
+ j < len && toSelector( tokens )
+ );
+ }
+ matchers.push( matcher );
+ }
+ }
+
+ return elementMatcher( matchers );
+}
+
+function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
+ var bySet = setMatchers.length > 0,
+ byElement = elementMatchers.length > 0,
+ superMatcher = function( seed, context, xml, results, outermost ) {
+ var elem, j, matcher,
+ matchedCount = 0,
+ i = "0",
+ unmatched = seed && [],
+ setMatched = [],
+ contextBackup = outermostContext,
+ // We must always have either seed elements or outermost context
+ elems = seed || byElement && Expr.find["TAG"]( "*", outermost ),
+ // Use integer dirruns iff this is the outermost matcher
+ dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1),
+ len = elems.length;
+
+ if ( outermost ) {
+ outermostContext = context === document || context || outermost;
+ }
+
+ // Add elements passing elementMatchers directly to results
+ // Support: IE<9, Safari
+ // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id
+ for ( ; i !== len && (elem = elems[i]) != null; i++ ) {
+ if ( byElement && elem ) {
+ j = 0;
+ if ( !context && elem.ownerDocument !== document ) {
+ setDocument( elem );
+ xml = !documentIsHTML;
+ }
+ while ( (matcher = elementMatchers[j++]) ) {
+ if ( matcher( elem, context || document, xml) ) {
+ results.push( elem );
+ break;
+ }
+ }
+ if ( outermost ) {
+ dirruns = dirrunsUnique;
+ }
+ }
+
+ // Track unmatched elements for set filters
+ if ( bySet ) {
+ // They will have gone through all possible matchers
+ if ( (elem = !matcher && elem) ) {
+ matchedCount--;
+ }
+
+ // Lengthen the array for every element, matched or not
+ if ( seed ) {
+ unmatched.push( elem );
+ }
+ }
+ }
+
+ // `i` is now the count of elements visited above, and adding it to `matchedCount`
+ // makes the latter nonnegative.
+ matchedCount += i;
+
+ // Apply set filters to unmatched elements
+ // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount`
+ // equals `i`), unless we didn't visit _any_ elements in the above loop because we have
+ // no element matchers and no seed.
+ // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that
+ // case, which will result in a "00" `matchedCount` that differs from `i` but is also
+ // numerically zero.
+ if ( bySet && i !== matchedCount ) {
+ j = 0;
+ while ( (matcher = setMatchers[j++]) ) {
+ matcher( unmatched, setMatched, context, xml );
+ }
+
+ if ( seed ) {
+ // Reintegrate element matches to eliminate the need for sorting
+ if ( matchedCount > 0 ) {
+ while ( i-- ) {
+ if ( !(unmatched[i] || setMatched[i]) ) {
+ setMatched[i] = pop.call( results );
+ }
+ }
+ }
+
+ // Discard index placeholder values to get only actual matches
+ setMatched = condense( setMatched );
+ }
+
+ // Add matches to results
+ push.apply( results, setMatched );
+
+ // Seedless set matches succeeding multiple successful matchers stipulate sorting
+ if ( outermost && !seed && setMatched.length > 0 &&
+ ( matchedCount + setMatchers.length ) > 1 ) {
+
+ Sizzle.uniqueSort( results );
+ }
+ }
+
+ // Override manipulation of globals by nested matchers
+ if ( outermost ) {
+ dirruns = dirrunsUnique;
+ outermostContext = contextBackup;
+ }
+
+ return unmatched;
+ };
+
+ return bySet ?
+ markFunction( superMatcher ) :
+ superMatcher;
+}
+
+compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {
+ var i,
+ setMatchers = [],
+ elementMatchers = [],
+ cached = compilerCache[ selector + " " ];
+
+ if ( !cached ) {
+ // Generate a function of recursive functions that can be used to check each element
+ if ( !match ) {
+ match = tokenize( selector );
+ }
+ i = match.length;
+ while ( i-- ) {
+ cached = matcherFromTokens( match[i] );
+ if ( cached[ expando ] ) {
+ setMatchers.push( cached );
+ } else {
+ elementMatchers.push( cached );
+ }
+ }
+
+ // Cache the compiled function
+ cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );
+
+ // Save selector and tokenization
+ cached.selector = selector;
+ }
+ return cached;
+};
+
+/**
+ * A low-level selection function that works with Sizzle's compiled
+ * selector functions
+ * @param {String|Function} selector A selector or a pre-compiled
+ * selector function built with Sizzle.compile
+ * @param {Element} context
+ * @param {Array} [results]
+ * @param {Array} [seed] A set of elements to match against
+ */
+select = Sizzle.select = function( selector, context, results, seed ) {
+ var i, tokens, token, type, find,
+ compiled = typeof selector === "function" && selector,
+ match = !seed && tokenize( (selector = compiled.selector || selector) );
+
+ results = results || [];
+
+ // Try to minimize operations if there is only one selector in the list and no seed
+ // (the latter of which guarantees us context)
+ if ( match.length === 1 ) {
+
+ // Reduce context if the leading compound selector is an ID
+ tokens = match[0] = match[0].slice( 0 );
+ if ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&
+ context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[1].type ] ) {
+
+ context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];
+ if ( !context ) {
+ return results;
+
+ // Precompiled matchers will still verify ancestry, so step up a level
+ } else if ( compiled ) {
+ context = context.parentNode;
+ }
+
+ selector = selector.slice( tokens.shift().value.length );
+ }
+
+ // Fetch a seed set for right-to-left matching
+ i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length;
+ while ( i-- ) {
+ token = tokens[i];
+
+ // Abort if we hit a combinator
+ if ( Expr.relative[ (type = token.type) ] ) {
+ break;
+ }
+ if ( (find = Expr.find[ type ]) ) {
+ // Search, expanding context for leading sibling combinators
+ if ( (seed = find(
+ token.matches[0].replace( runescape, funescape ),
+ rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context
+ )) ) {
+
+ // If seed is empty or no tokens remain, we can return early
+ tokens.splice( i, 1 );
+ selector = seed.length && toSelector( tokens );
+ if ( !selector ) {
+ push.apply( results, seed );
+ return results;
+ }
+
+ break;
+ }
+ }
+ }
+ }
+
+ // Compile and execute a filtering function if one is not provided
+ // Provide `match` to avoid retokenization if we modified the selector above
+ ( compiled || compile( selector, match ) )(
+ seed,
+ context,
+ !documentIsHTML,
+ results,
+ !context || rsibling.test( selector ) && testContext( context.parentNode ) || context
+ );
+ return results;
+};
+
+// One-time assignments
+
+// Sort stability
+support.sortStable = expando.split("").sort( sortOrder ).join("") === expando;
+
+// Support: Chrome 14-35+
+// Always assume duplicates if they aren't passed to the comparison function
+support.detectDuplicates = !!hasDuplicate;
+
+// Initialize against the default document
+setDocument();
+
+// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)
+// Detached nodes confoundingly follow *each other*
+support.sortDetached = assert(function( el ) {
+ // Should return 1, but returns 4 (following)
+ return el.compareDocumentPosition( document.createElement("fieldset") ) & 1;
+});
+
+// Support: IE<8
+// Prevent attribute/property "interpolation"
+// https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
+if ( !assert(function( el ) {
+ el.innerHTML = "";
+ return el.firstChild.getAttribute("href") === "#" ;
+}) ) {
+ addHandle( "type|href|height|width", function( elem, name, isXML ) {
+ if ( !isXML ) {
+ return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 );
+ }
+ });
+}
+
+// Support: IE<9
+// Use defaultValue in place of getAttribute("value")
+if ( !support.attributes || !assert(function( el ) {
+ el.innerHTML = "";
+ el.firstChild.setAttribute( "value", "" );
+ return el.firstChild.getAttribute( "value" ) === "";
+}) ) {
+ addHandle( "value", function( elem, name, isXML ) {
+ if ( !isXML && elem.nodeName.toLowerCase() === "input" ) {
+ return elem.defaultValue;
+ }
+ });
+}
+
+// Support: IE<9
+// Use getAttributeNode to fetch booleans when getAttribute lies
+if ( !assert(function( el ) {
+ return el.getAttribute("disabled") == null;
+}) ) {
+ addHandle( booleans, function( elem, name, isXML ) {
+ var val;
+ if ( !isXML ) {
+ return elem[ name ] === true ? name.toLowerCase() :
+ (val = elem.getAttributeNode( name )) && val.specified ?
+ val.value :
+ null;
+ }
+ });
+}
+
+return Sizzle;
+
+})( window );
+
+
+
+jQuery.find = Sizzle;
+jQuery.expr = Sizzle.selectors;
+
+// Deprecated
+jQuery.expr[ ":" ] = jQuery.expr.pseudos;
+jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort;
+jQuery.text = Sizzle.getText;
+jQuery.isXMLDoc = Sizzle.isXML;
+jQuery.contains = Sizzle.contains;
+jQuery.escapeSelector = Sizzle.escape;
+
+
+
+
+var dir = function( elem, dir, until ) {
+ var matched = [],
+ truncate = until !== undefined;
+
+ while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) {
+ if ( elem.nodeType === 1 ) {
+ if ( truncate && jQuery( elem ).is( until ) ) {
+ break;
+ }
+ matched.push( elem );
+ }
+ }
+ return matched;
+};
+
+
+var siblings = function( n, elem ) {
+ var matched = [];
+
+ for ( ; n; n = n.nextSibling ) {
+ if ( n.nodeType === 1 && n !== elem ) {
+ matched.push( n );
+ }
+ }
+
+ return matched;
+};
+
+
+var rneedsContext = jQuery.expr.match.needsContext;
+
+
+
+function nodeName( elem, name ) {
+
+ return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
+
+};
+var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i );
+
+
+
+var risSimple = /^.[^:#\[\.,]*$/;
+
+// Implement the identical functionality for filter and not
+function winnow( elements, qualifier, not ) {
+ if ( jQuery.isFunction( qualifier ) ) {
+ return jQuery.grep( elements, function( elem, i ) {
+ return !!qualifier.call( elem, i, elem ) !== not;
+ } );
+ }
+
+ // Single element
+ if ( qualifier.nodeType ) {
+ return jQuery.grep( elements, function( elem ) {
+ return ( elem === qualifier ) !== not;
+ } );
+ }
+
+ // Arraylike of elements (jQuery, arguments, Array)
+ if ( typeof qualifier !== "string" ) {
+ return jQuery.grep( elements, function( elem ) {
+ return ( indexOf.call( qualifier, elem ) > -1 ) !== not;
+ } );
+ }
+
+ // Simple selector that can be filtered directly, removing non-Elements
+ if ( risSimple.test( qualifier ) ) {
+ return jQuery.filter( qualifier, elements, not );
+ }
+
+ // Complex selector, compare the two sets, removing non-Elements
+ qualifier = jQuery.filter( qualifier, elements );
+ return jQuery.grep( elements, function( elem ) {
+ return ( indexOf.call( qualifier, elem ) > -1 ) !== not && elem.nodeType === 1;
+ } );
+}
+
+jQuery.filter = function( expr, elems, not ) {
+ var elem = elems[ 0 ];
+
+ if ( not ) {
+ expr = ":not(" + expr + ")";
+ }
+
+ if ( elems.length === 1 && elem.nodeType === 1 ) {
+ return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [];
+ }
+
+ return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {
+ return elem.nodeType === 1;
+ } ) );
+};
+
+jQuery.fn.extend( {
+ find: function( selector ) {
+ var i, ret,
+ len = this.length,
+ self = this;
+
+ if ( typeof selector !== "string" ) {
+ return this.pushStack( jQuery( selector ).filter( function() {
+ for ( i = 0; i < len; i++ ) {
+ if ( jQuery.contains( self[ i ], this ) ) {
+ return true;
+ }
+ }
+ } ) );
+ }
+
+ ret = this.pushStack( [] );
+
+ for ( i = 0; i < len; i++ ) {
+ jQuery.find( selector, self[ i ], ret );
+ }
+
+ return len > 1 ? jQuery.uniqueSort( ret ) : ret;
+ },
+ filter: function( selector ) {
+ return this.pushStack( winnow( this, selector || [], false ) );
+ },
+ not: function( selector ) {
+ return this.pushStack( winnow( this, selector || [], true ) );
+ },
+ is: function( selector ) {
+ return !!winnow(
+ this,
+
+ // If this is a positional/relative selector, check membership in the returned set
+ // so $("p:first").is("p:last") won't return true for a doc with two "p".
+ typeof selector === "string" && rneedsContext.test( selector ) ?
+ jQuery( selector ) :
+ selector || [],
+ false
+ ).length;
+ }
+} );
+
+
+// Initialize a jQuery object
+
+
+// A central reference to the root jQuery(document)
+var rootjQuery,
+
+ // A simple way to check for HTML strings
+ // Prioritize #id over to avoid XSS via location.hash (#9521)
+ // Strict HTML recognition (#11290: must start with <)
+ // Shortcut simple #id case for speed
+ rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,
+
+ init = jQuery.fn.init = function( selector, context, root ) {
+ var match, elem;
+
+ // HANDLE: $(""), $(null), $(undefined), $(false)
+ if ( !selector ) {
+ return this;
+ }
+
+ // Method init() accepts an alternate rootjQuery
+ // so migrate can support jQuery.sub (gh-2101)
+ root = root || rootjQuery;
+
+ // Handle HTML strings
+ if ( typeof selector === "string" ) {
+ if ( selector[ 0 ] === "<" &&
+ selector[ selector.length - 1 ] === ">" &&
+ selector.length >= 3 ) {
+
+ // Assume that strings that start and end with <> are HTML and skip the regex check
+ match = [ null, selector, null ];
+
+ } else {
+ match = rquickExpr.exec( selector );
+ }
+
+ // Match html or make sure no context is specified for #id
+ if ( match && ( match[ 1 ] || !context ) ) {
+
+ // HANDLE: $(html) -> $(array)
+ if ( match[ 1 ] ) {
+ context = context instanceof jQuery ? context[ 0 ] : context;
+
+ // Option to run scripts is true for back-compat
+ // Intentionally let the error be thrown if parseHTML is not present
+ jQuery.merge( this, jQuery.parseHTML(
+ match[ 1 ],
+ context && context.nodeType ? context.ownerDocument || context : document,
+ true
+ ) );
+
+ // HANDLE: $(html, props)
+ if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) {
+ for ( match in context ) {
+
+ // Properties of context are called as methods if possible
+ if ( jQuery.isFunction( this[ match ] ) ) {
+ this[ match ]( context[ match ] );
+
+ // ...and otherwise set as attributes
+ } else {
+ this.attr( match, context[ match ] );
+ }
+ }
+ }
+
+ return this;
+
+ // HANDLE: $(#id)
+ } else {
+ elem = document.getElementById( match[ 2 ] );
+
+ if ( elem ) {
+
+ // Inject the element directly into the jQuery object
+ this[ 0 ] = elem;
+ this.length = 1;
+ }
+ return this;
+ }
+
+ // HANDLE: $(expr, $(...))
+ } else if ( !context || context.jquery ) {
+ return ( context || root ).find( selector );
+
+ // HANDLE: $(expr, context)
+ // (which is just equivalent to: $(context).find(expr)
+ } else {
+ return this.constructor( context ).find( selector );
+ }
+
+ // HANDLE: $(DOMElement)
+ } else if ( selector.nodeType ) {
+ this[ 0 ] = selector;
+ this.length = 1;
+ return this;
+
+ // HANDLE: $(function)
+ // Shortcut for document ready
+ } else if ( jQuery.isFunction( selector ) ) {
+ return root.ready !== undefined ?
+ root.ready( selector ) :
+
+ // Execute immediately if ready is not present
+ selector( jQuery );
+ }
+
+ return jQuery.makeArray( selector, this );
+ };
+
+// Give the init function the jQuery prototype for later instantiation
+init.prototype = jQuery.fn;
+
+// Initialize central reference
+rootjQuery = jQuery( document );
+
+
+var rparentsprev = /^(?:parents|prev(?:Until|All))/,
+
+ // Methods guaranteed to produce a unique set when starting from a unique set
+ guaranteedUnique = {
+ children: true,
+ contents: true,
+ next: true,
+ prev: true
+ };
+
+jQuery.fn.extend( {
+ has: function( target ) {
+ var targets = jQuery( target, this ),
+ l = targets.length;
+
+ return this.filter( function() {
+ var i = 0;
+ for ( ; i < l; i++ ) {
+ if ( jQuery.contains( this, targets[ i ] ) ) {
+ return true;
+ }
+ }
+ } );
+ },
+
+ closest: function( selectors, context ) {
+ var cur,
+ i = 0,
+ l = this.length,
+ matched = [],
+ targets = typeof selectors !== "string" && jQuery( selectors );
+
+ // Positional selectors never match, since there's no _selection_ context
+ if ( !rneedsContext.test( selectors ) ) {
+ for ( ; i < l; i++ ) {
+ for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) {
+
+ // Always skip document fragments
+ if ( cur.nodeType < 11 && ( targets ?
+ targets.index( cur ) > -1 :
+
+ // Don't pass non-elements to Sizzle
+ cur.nodeType === 1 &&
+ jQuery.find.matchesSelector( cur, selectors ) ) ) {
+
+ matched.push( cur );
+ break;
+ }
+ }
+ }
+ }
+
+ return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched );
+ },
+
+ // Determine the position of an element within the set
+ index: function( elem ) {
+
+ // No argument, return index in parent
+ if ( !elem ) {
+ return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1;
+ }
+
+ // Index in selector
+ if ( typeof elem === "string" ) {
+ return indexOf.call( jQuery( elem ), this[ 0 ] );
+ }
+
+ // Locate the position of the desired element
+ return indexOf.call( this,
+
+ // If it receives a jQuery object, the first element is used
+ elem.jquery ? elem[ 0 ] : elem
+ );
+ },
+
+ add: function( selector, context ) {
+ return this.pushStack(
+ jQuery.uniqueSort(
+ jQuery.merge( this.get(), jQuery( selector, context ) )
+ )
+ );
+ },
+
+ addBack: function( selector ) {
+ return this.add( selector == null ?
+ this.prevObject : this.prevObject.filter( selector )
+ );
+ }
+} );
+
+function sibling( cur, dir ) {
+ while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {}
+ return cur;
+}
+
+jQuery.each( {
+ parent: function( elem ) {
+ var parent = elem.parentNode;
+ return parent && parent.nodeType !== 11 ? parent : null;
+ },
+ parents: function( elem ) {
+ return dir( elem, "parentNode" );
+ },
+ parentsUntil: function( elem, i, until ) {
+ return dir( elem, "parentNode", until );
+ },
+ next: function( elem ) {
+ return sibling( elem, "nextSibling" );
+ },
+ prev: function( elem ) {
+ return sibling( elem, "previousSibling" );
+ },
+ nextAll: function( elem ) {
+ return dir( elem, "nextSibling" );
+ },
+ prevAll: function( elem ) {
+ return dir( elem, "previousSibling" );
+ },
+ nextUntil: function( elem, i, until ) {
+ return dir( elem, "nextSibling", until );
+ },
+ prevUntil: function( elem, i, until ) {
+ return dir( elem, "previousSibling", until );
+ },
+ siblings: function( elem ) {
+ return siblings( ( elem.parentNode || {} ).firstChild, elem );
+ },
+ children: function( elem ) {
+ return siblings( elem.firstChild );
+ },
+ contents: function( elem ) {
+ if ( nodeName( elem, "iframe" ) ) {
+ return elem.contentDocument;
+ }
+
+ // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only
+ // Treat the template element as a regular one in browsers that
+ // don't support it.
+ if ( nodeName( elem, "template" ) ) {
+ elem = elem.content || elem;
+ }
+
+ return jQuery.merge( [], elem.childNodes );
+ }
+}, function( name, fn ) {
+ jQuery.fn[ name ] = function( until, selector ) {
+ var matched = jQuery.map( this, fn, until );
+
+ if ( name.slice( -5 ) !== "Until" ) {
+ selector = until;
+ }
+
+ if ( selector && typeof selector === "string" ) {
+ matched = jQuery.filter( selector, matched );
+ }
+
+ if ( this.length > 1 ) {
+
+ // Remove duplicates
+ if ( !guaranteedUnique[ name ] ) {
+ jQuery.uniqueSort( matched );
+ }
+
+ // Reverse order for parents* and prev-derivatives
+ if ( rparentsprev.test( name ) ) {
+ matched.reverse();
+ }
+ }
+
+ return this.pushStack( matched );
+ };
+} );
+var rnothtmlwhite = ( /[^\x20\t\r\n\f]+/g );
+
+
+
+// Convert String-formatted options into Object-formatted ones
+function createOptions( options ) {
+ var object = {};
+ jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) {
+ object[ flag ] = true;
+ } );
+ return object;
+}
+
+/*
+ * Create a callback list using the following parameters:
+ *
+ * options: an optional list of space-separated options that will change how
+ * the callback list behaves or a more traditional option object
+ *
+ * By default a callback list will act like an event callback list and can be
+ * "fired" multiple times.
+ *
+ * Possible options:
+ *
+ * once: will ensure the callback list can only be fired once (like a Deferred)
+ *
+ * memory: will keep track of previous values and will call any callback added
+ * after the list has been fired right away with the latest "memorized"
+ * values (like a Deferred)
+ *
+ * unique: will ensure a callback can only be added once (no duplicate in the list)
+ *
+ * stopOnFalse: interrupt callings when a callback returns false
+ *
+ */
+jQuery.Callbacks = function( options ) {
+
+ // Convert options from String-formatted to Object-formatted if needed
+ // (we check in cache first)
+ options = typeof options === "string" ?
+ createOptions( options ) :
+ jQuery.extend( {}, options );
+
+ var // Flag to know if list is currently firing
+ firing,
+
+ // Last fire value for non-forgettable lists
+ memory,
+
+ // Flag to know if list was already fired
+ fired,
+
+ // Flag to prevent firing
+ locked,
+
+ // Actual callback list
+ list = [],
+
+ // Queue of execution data for repeatable lists
+ queue = [],
+
+ // Index of currently firing callback (modified by add/remove as needed)
+ firingIndex = -1,
+
+ // Fire callbacks
+ fire = function() {
+
+ // Enforce single-firing
+ locked = locked || options.once;
+
+ // Execute callbacks for all pending executions,
+ // respecting firingIndex overrides and runtime changes
+ fired = firing = true;
+ for ( ; queue.length; firingIndex = -1 ) {
+ memory = queue.shift();
+ while ( ++firingIndex < list.length ) {
+
+ // Run callback and check for early termination
+ if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false &&
+ options.stopOnFalse ) {
+
+ // Jump to end and forget the data so .add doesn't re-fire
+ firingIndex = list.length;
+ memory = false;
+ }
+ }
+ }
+
+ // Forget the data if we're done with it
+ if ( !options.memory ) {
+ memory = false;
+ }
+
+ firing = false;
+
+ // Clean up if we're done firing for good
+ if ( locked ) {
+
+ // Keep an empty list if we have data for future add calls
+ if ( memory ) {
+ list = [];
+
+ // Otherwise, this object is spent
+ } else {
+ list = "";
+ }
+ }
+ },
+
+ // Actual Callbacks object
+ self = {
+
+ // Add a callback or a collection of callbacks to the list
+ add: function() {
+ if ( list ) {
+
+ // If we have memory from a past run, we should fire after adding
+ if ( memory && !firing ) {
+ firingIndex = list.length - 1;
+ queue.push( memory );
+ }
+
+ ( function add( args ) {
+ jQuery.each( args, function( _, arg ) {
+ if ( jQuery.isFunction( arg ) ) {
+ if ( !options.unique || !self.has( arg ) ) {
+ list.push( arg );
+ }
+ } else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) {
+
+ // Inspect recursively
+ add( arg );
+ }
+ } );
+ } )( arguments );
+
+ if ( memory && !firing ) {
+ fire();
+ }
+ }
+ return this;
+ },
+
+ // Remove a callback from the list
+ remove: function() {
+ jQuery.each( arguments, function( _, arg ) {
+ var index;
+ while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
+ list.splice( index, 1 );
+
+ // Handle firing indexes
+ if ( index <= firingIndex ) {
+ firingIndex--;
+ }
+ }
+ } );
+ return this;
+ },
+
+ // Check if a given callback is in the list.
+ // If no argument is given, return whether or not list has callbacks attached.
+ has: function( fn ) {
+ return fn ?
+ jQuery.inArray( fn, list ) > -1 :
+ list.length > 0;
+ },
+
+ // Remove all callbacks from the list
+ empty: function() {
+ if ( list ) {
+ list = [];
+ }
+ return this;
+ },
+
+ // Disable .fire and .add
+ // Abort any current/pending executions
+ // Clear all callbacks and values
+ disable: function() {
+ locked = queue = [];
+ list = memory = "";
+ return this;
+ },
+ disabled: function() {
+ return !list;
+ },
+
+ // Disable .fire
+ // Also disable .add unless we have memory (since it would have no effect)
+ // Abort any pending executions
+ lock: function() {
+ locked = queue = [];
+ if ( !memory && !firing ) {
+ list = memory = "";
+ }
+ return this;
+ },
+ locked: function() {
+ return !!locked;
+ },
+
+ // Call all callbacks with the given context and arguments
+ fireWith: function( context, args ) {
+ if ( !locked ) {
+ args = args || [];
+ args = [ context, args.slice ? args.slice() : args ];
+ queue.push( args );
+ if ( !firing ) {
+ fire();
+ }
+ }
+ return this;
+ },
+
+ // Call all the callbacks with the given arguments
+ fire: function() {
+ self.fireWith( this, arguments );
+ return this;
+ },
+
+ // To know if the callbacks have already been called at least once
+ fired: function() {
+ return !!fired;
+ }
+ };
+
+ return self;
+};
+
+
+function Identity( v ) {
+ return v;
+}
+function Thrower( ex ) {
+ throw ex;
+}
+
+function adoptValue( value, resolve, reject, noValue ) {
+ var method;
+
+ try {
+
+ // Check for promise aspect first to privilege synchronous behavior
+ if ( value && jQuery.isFunction( ( method = value.promise ) ) ) {
+ method.call( value ).done( resolve ).fail( reject );
+
+ // Other thenables
+ } else if ( value && jQuery.isFunction( ( method = value.then ) ) ) {
+ method.call( value, resolve, reject );
+
+ // Other non-thenables
+ } else {
+
+ // Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer:
+ // * false: [ value ].slice( 0 ) => resolve( value )
+ // * true: [ value ].slice( 1 ) => resolve()
+ resolve.apply( undefined, [ value ].slice( noValue ) );
+ }
+
+ // For Promises/A+, convert exceptions into rejections
+ // Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in
+ // Deferred#then to conditionally suppress rejection.
+ } catch ( value ) {
+
+ // Support: Android 4.0 only
+ // Strict mode functions invoked without .call/.apply get global-object context
+ reject.apply( undefined, [ value ] );
+ }
+}
+
+jQuery.extend( {
+
+ Deferred: function( func ) {
+ var tuples = [
+
+ // action, add listener, callbacks,
+ // ... .then handlers, argument index, [final state]
+ [ "notify", "progress", jQuery.Callbacks( "memory" ),
+ jQuery.Callbacks( "memory" ), 2 ],
+ [ "resolve", "done", jQuery.Callbacks( "once memory" ),
+ jQuery.Callbacks( "once memory" ), 0, "resolved" ],
+ [ "reject", "fail", jQuery.Callbacks( "once memory" ),
+ jQuery.Callbacks( "once memory" ), 1, "rejected" ]
+ ],
+ state = "pending",
+ promise = {
+ state: function() {
+ return state;
+ },
+ always: function() {
+ deferred.done( arguments ).fail( arguments );
+ return this;
+ },
+ "catch": function( fn ) {
+ return promise.then( null, fn );
+ },
+
+ // Keep pipe for back-compat
+ pipe: function( /* fnDone, fnFail, fnProgress */ ) {
+ var fns = arguments;
+
+ return jQuery.Deferred( function( newDefer ) {
+ jQuery.each( tuples, function( i, tuple ) {
+
+ // Map tuples (progress, done, fail) to arguments (done, fail, progress)
+ var fn = jQuery.isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ];
+
+ // deferred.progress(function() { bind to newDefer or newDefer.notify })
+ // deferred.done(function() { bind to newDefer or newDefer.resolve })
+ // deferred.fail(function() { bind to newDefer or newDefer.reject })
+ deferred[ tuple[ 1 ] ]( function() {
+ var returned = fn && fn.apply( this, arguments );
+ if ( returned && jQuery.isFunction( returned.promise ) ) {
+ returned.promise()
+ .progress( newDefer.notify )
+ .done( newDefer.resolve )
+ .fail( newDefer.reject );
+ } else {
+ newDefer[ tuple[ 0 ] + "With" ](
+ this,
+ fn ? [ returned ] : arguments
+ );
+ }
+ } );
+ } );
+ fns = null;
+ } ).promise();
+ },
+ then: function( onFulfilled, onRejected, onProgress ) {
+ var maxDepth = 0;
+ function resolve( depth, deferred, handler, special ) {
+ return function() {
+ var that = this,
+ args = arguments,
+ mightThrow = function() {
+ var returned, then;
+
+ // Support: Promises/A+ section 2.3.3.3.3
+ // https://promisesaplus.com/#point-59
+ // Ignore double-resolution attempts
+ if ( depth < maxDepth ) {
+ return;
+ }
+
+ returned = handler.apply( that, args );
+
+ // Support: Promises/A+ section 2.3.1
+ // https://promisesaplus.com/#point-48
+ if ( returned === deferred.promise() ) {
+ throw new TypeError( "Thenable self-resolution" );
+ }
+
+ // Support: Promises/A+ sections 2.3.3.1, 3.5
+ // https://promisesaplus.com/#point-54
+ // https://promisesaplus.com/#point-75
+ // Retrieve `then` only once
+ then = returned &&
+
+ // Support: Promises/A+ section 2.3.4
+ // https://promisesaplus.com/#point-64
+ // Only check objects and functions for thenability
+ ( typeof returned === "object" ||
+ typeof returned === "function" ) &&
+ returned.then;
+
+ // Handle a returned thenable
+ if ( jQuery.isFunction( then ) ) {
+
+ // Special processors (notify) just wait for resolution
+ if ( special ) {
+ then.call(
+ returned,
+ resolve( maxDepth, deferred, Identity, special ),
+ resolve( maxDepth, deferred, Thrower, special )
+ );
+
+ // Normal processors (resolve) also hook into progress
+ } else {
+
+ // ...and disregard older resolution values
+ maxDepth++;
+
+ then.call(
+ returned,
+ resolve( maxDepth, deferred, Identity, special ),
+ resolve( maxDepth, deferred, Thrower, special ),
+ resolve( maxDepth, deferred, Identity,
+ deferred.notifyWith )
+ );
+ }
+
+ // Handle all other returned values
+ } else {
+
+ // Only substitute handlers pass on context
+ // and multiple values (non-spec behavior)
+ if ( handler !== Identity ) {
+ that = undefined;
+ args = [ returned ];
+ }
+
+ // Process the value(s)
+ // Default process is resolve
+ ( special || deferred.resolveWith )( that, args );
+ }
+ },
+
+ // Only normal processors (resolve) catch and reject exceptions
+ process = special ?
+ mightThrow :
+ function() {
+ try {
+ mightThrow();
+ } catch ( e ) {
+
+ if ( jQuery.Deferred.exceptionHook ) {
+ jQuery.Deferred.exceptionHook( e,
+ process.stackTrace );
+ }
+
+ // Support: Promises/A+ section 2.3.3.3.4.1
+ // https://promisesaplus.com/#point-61
+ // Ignore post-resolution exceptions
+ if ( depth + 1 >= maxDepth ) {
+
+ // Only substitute handlers pass on context
+ // and multiple values (non-spec behavior)
+ if ( handler !== Thrower ) {
+ that = undefined;
+ args = [ e ];
+ }
+
+ deferred.rejectWith( that, args );
+ }
+ }
+ };
+
+ // Support: Promises/A+ section 2.3.3.3.1
+ // https://promisesaplus.com/#point-57
+ // Re-resolve promises immediately to dodge false rejection from
+ // subsequent errors
+ if ( depth ) {
+ process();
+ } else {
+
+ // Call an optional hook to record the stack, in case of exception
+ // since it's otherwise lost when execution goes async
+ if ( jQuery.Deferred.getStackHook ) {
+ process.stackTrace = jQuery.Deferred.getStackHook();
+ }
+ window.setTimeout( process );
+ }
+ };
+ }
+
+ return jQuery.Deferred( function( newDefer ) {
+
+ // progress_handlers.add( ... )
+ tuples[ 0 ][ 3 ].add(
+ resolve(
+ 0,
+ newDefer,
+ jQuery.isFunction( onProgress ) ?
+ onProgress :
+ Identity,
+ newDefer.notifyWith
+ )
+ );
+
+ // fulfilled_handlers.add( ... )
+ tuples[ 1 ][ 3 ].add(
+ resolve(
+ 0,
+ newDefer,
+ jQuery.isFunction( onFulfilled ) ?
+ onFulfilled :
+ Identity
+ )
+ );
+
+ // rejected_handlers.add( ... )
+ tuples[ 2 ][ 3 ].add(
+ resolve(
+ 0,
+ newDefer,
+ jQuery.isFunction( onRejected ) ?
+ onRejected :
+ Thrower
+ )
+ );
+ } ).promise();
+ },
+
+ // Get a promise for this deferred
+ // If obj is provided, the promise aspect is added to the object
+ promise: function( obj ) {
+ return obj != null ? jQuery.extend( obj, promise ) : promise;
+ }
+ },
+ deferred = {};
+
+ // Add list-specific methods
+ jQuery.each( tuples, function( i, tuple ) {
+ var list = tuple[ 2 ],
+ stateString = tuple[ 5 ];
+
+ // promise.progress = list.add
+ // promise.done = list.add
+ // promise.fail = list.add
+ promise[ tuple[ 1 ] ] = list.add;
+
+ // Handle state
+ if ( stateString ) {
+ list.add(
+ function() {
+
+ // state = "resolved" (i.e., fulfilled)
+ // state = "rejected"
+ state = stateString;
+ },
+
+ // rejected_callbacks.disable
+ // fulfilled_callbacks.disable
+ tuples[ 3 - i ][ 2 ].disable,
+
+ // progress_callbacks.lock
+ tuples[ 0 ][ 2 ].lock
+ );
+ }
+
+ // progress_handlers.fire
+ // fulfilled_handlers.fire
+ // rejected_handlers.fire
+ list.add( tuple[ 3 ].fire );
+
+ // deferred.notify = function() { deferred.notifyWith(...) }
+ // deferred.resolve = function() { deferred.resolveWith(...) }
+ // deferred.reject = function() { deferred.rejectWith(...) }
+ deferred[ tuple[ 0 ] ] = function() {
+ deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments );
+ return this;
+ };
+
+ // deferred.notifyWith = list.fireWith
+ // deferred.resolveWith = list.fireWith
+ // deferred.rejectWith = list.fireWith
+ deferred[ tuple[ 0 ] + "With" ] = list.fireWith;
+ } );
+
+ // Make the deferred a promise
+ promise.promise( deferred );
+
+ // Call given func if any
+ if ( func ) {
+ func.call( deferred, deferred );
+ }
+
+ // All done!
+ return deferred;
+ },
+
+ // Deferred helper
+ when: function( singleValue ) {
+ var
+
+ // count of uncompleted subordinates
+ remaining = arguments.length,
+
+ // count of unprocessed arguments
+ i = remaining,
+
+ // subordinate fulfillment data
+ resolveContexts = Array( i ),
+ resolveValues = slice.call( arguments ),
+
+ // the master Deferred
+ master = jQuery.Deferred(),
+
+ // subordinate callback factory
+ updateFunc = function( i ) {
+ return function( value ) {
+ resolveContexts[ i ] = this;
+ resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;
+ if ( !( --remaining ) ) {
+ master.resolveWith( resolveContexts, resolveValues );
+ }
+ };
+ };
+
+ // Single- and empty arguments are adopted like Promise.resolve
+ if ( remaining <= 1 ) {
+ adoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject,
+ !remaining );
+
+ // Use .then() to unwrap secondary thenables (cf. gh-3000)
+ if ( master.state() === "pending" ||
+ jQuery.isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) {
+
+ return master.then();
+ }
+ }
+
+ // Multiple arguments are aggregated like Promise.all array elements
+ while ( i-- ) {
+ adoptValue( resolveValues[ i ], updateFunc( i ), master.reject );
+ }
+
+ return master.promise();
+ }
+} );
+
+
+// These usually indicate a programmer mistake during development,
+// warn about them ASAP rather than swallowing them by default.
+var rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;
+
+jQuery.Deferred.exceptionHook = function( error, stack ) {
+
+ // Support: IE 8 - 9 only
+ // Console exists when dev tools are open, which can happen at any time
+ if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) {
+ window.console.warn( "jQuery.Deferred exception: " + error.message, error.stack, stack );
+ }
+};
+
+
+
+
+jQuery.readyException = function( error ) {
+ window.setTimeout( function() {
+ throw error;
+ } );
+};
+
+
+
+
+// The deferred used on DOM ready
+var readyList = jQuery.Deferred();
+
+jQuery.fn.ready = function( fn ) {
+
+ readyList
+ .then( fn )
+
+ // Wrap jQuery.readyException in a function so that the lookup
+ // happens at the time of error handling instead of callback
+ // registration.
+ .catch( function( error ) {
+ jQuery.readyException( error );
+ } );
+
+ return this;
+};
+
+jQuery.extend( {
+
+ // Is the DOM ready to be used? Set to true once it occurs.
+ isReady: false,
+
+ // A counter to track how many items to wait for before
+ // the ready event fires. See #6781
+ readyWait: 1,
+
+ // Handle when the DOM is ready
+ ready: function( wait ) {
+
+ // Abort if there are pending holds or we're already ready
+ if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {
+ return;
+ }
+
+ // Remember that the DOM is ready
+ jQuery.isReady = true;
+
+ // If a normal DOM Ready event fired, decrement, and wait if need be
+ if ( wait !== true && --jQuery.readyWait > 0 ) {
+ return;
+ }
+
+ // If there are functions bound, to execute
+ readyList.resolveWith( document, [ jQuery ] );
+ }
+} );
+
+jQuery.ready.then = readyList.then;
+
+// The ready event handler and self cleanup method
+function completed() {
+ document.removeEventListener( "DOMContentLoaded", completed );
+ window.removeEventListener( "load", completed );
+ jQuery.ready();
+}
+
+// Catch cases where $(document).ready() is called
+// after the browser event has already occurred.
+// Support: IE <=9 - 10 only
+// Older IE sometimes signals "interactive" too soon
+if ( document.readyState === "complete" ||
+ ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) {
+
+ // Handle it asynchronously to allow scripts the opportunity to delay ready
+ window.setTimeout( jQuery.ready );
+
+} else {
+
+ // Use the handy event callback
+ document.addEventListener( "DOMContentLoaded", completed );
+
+ // A fallback to window.onload, that will always work
+ window.addEventListener( "load", completed );
+}
+
+
+
+
+// Multifunctional method to get and set values of a collection
+// The value/s can optionally be executed if it's a function
+var access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
+ var i = 0,
+ len = elems.length,
+ bulk = key == null;
+
+ // Sets many values
+ if ( jQuery.type( key ) === "object" ) {
+ chainable = true;
+ for ( i in key ) {
+ access( elems, fn, i, key[ i ], true, emptyGet, raw );
+ }
+
+ // Sets one value
+ } else if ( value !== undefined ) {
+ chainable = true;
+
+ if ( !jQuery.isFunction( value ) ) {
+ raw = true;
+ }
+
+ if ( bulk ) {
+
+ // Bulk operations run against the entire set
+ if ( raw ) {
+ fn.call( elems, value );
+ fn = null;
+
+ // ...except when executing function values
+ } else {
+ bulk = fn;
+ fn = function( elem, key, value ) {
+ return bulk.call( jQuery( elem ), value );
+ };
+ }
+ }
+
+ if ( fn ) {
+ for ( ; i < len; i++ ) {
+ fn(
+ elems[ i ], key, raw ?
+ value :
+ value.call( elems[ i ], i, fn( elems[ i ], key ) )
+ );
+ }
+ }
+ }
+
+ if ( chainable ) {
+ return elems;
+ }
+
+ // Gets
+ if ( bulk ) {
+ return fn.call( elems );
+ }
+
+ return len ? fn( elems[ 0 ], key ) : emptyGet;
+};
+var acceptData = function( owner ) {
+
+ // Accepts only:
+ // - Node
+ // - Node.ELEMENT_NODE
+ // - Node.DOCUMENT_NODE
+ // - Object
+ // - Any
+ return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType );
+};
+
+
+
+
+function Data() {
+ this.expando = jQuery.expando + Data.uid++;
+}
+
+Data.uid = 1;
+
+Data.prototype = {
+
+ cache: function( owner ) {
+
+ // Check if the owner object already has a cache
+ var value = owner[ this.expando ];
+
+ // If not, create one
+ if ( !value ) {
+ value = {};
+
+ // We can accept data for non-element nodes in modern browsers,
+ // but we should not, see #8335.
+ // Always return an empty object.
+ if ( acceptData( owner ) ) {
+
+ // If it is a node unlikely to be stringify-ed or looped over
+ // use plain assignment
+ if ( owner.nodeType ) {
+ owner[ this.expando ] = value;
+
+ // Otherwise secure it in a non-enumerable property
+ // configurable must be true to allow the property to be
+ // deleted when data is removed
+ } else {
+ Object.defineProperty( owner, this.expando, {
+ value: value,
+ configurable: true
+ } );
+ }
+ }
+ }
+
+ return value;
+ },
+ set: function( owner, data, value ) {
+ var prop,
+ cache = this.cache( owner );
+
+ // Handle: [ owner, key, value ] args
+ // Always use camelCase key (gh-2257)
+ if ( typeof data === "string" ) {
+ cache[ jQuery.camelCase( data ) ] = value;
+
+ // Handle: [ owner, { properties } ] args
+ } else {
+
+ // Copy the properties one-by-one to the cache object
+ for ( prop in data ) {
+ cache[ jQuery.camelCase( prop ) ] = data[ prop ];
+ }
+ }
+ return cache;
+ },
+ get: function( owner, key ) {
+ return key === undefined ?
+ this.cache( owner ) :
+
+ // Always use camelCase key (gh-2257)
+ owner[ this.expando ] && owner[ this.expando ][ jQuery.camelCase( key ) ];
+ },
+ access: function( owner, key, value ) {
+
+ // In cases where either:
+ //
+ // 1. No key was specified
+ // 2. A string key was specified, but no value provided
+ //
+ // Take the "read" path and allow the get method to determine
+ // which value to return, respectively either:
+ //
+ // 1. The entire cache object
+ // 2. The data stored at the key
+ //
+ if ( key === undefined ||
+ ( ( key && typeof key === "string" ) && value === undefined ) ) {
+
+ return this.get( owner, key );
+ }
+
+ // When the key is not a string, or both a key and value
+ // are specified, set or extend (existing objects) with either:
+ //
+ // 1. An object of properties
+ // 2. A key and value
+ //
+ this.set( owner, key, value );
+
+ // Since the "set" path can have two possible entry points
+ // return the expected data based on which path was taken[*]
+ return value !== undefined ? value : key;
+ },
+ remove: function( owner, key ) {
+ var i,
+ cache = owner[ this.expando ];
+
+ if ( cache === undefined ) {
+ return;
+ }
+
+ if ( key !== undefined ) {
+
+ // Support array or space separated string of keys
+ if ( Array.isArray( key ) ) {
+
+ // If key is an array of keys...
+ // We always set camelCase keys, so remove that.
+ key = key.map( jQuery.camelCase );
+ } else {
+ key = jQuery.camelCase( key );
+
+ // If a key with the spaces exists, use it.
+ // Otherwise, create an array by matching non-whitespace
+ key = key in cache ?
+ [ key ] :
+ ( key.match( rnothtmlwhite ) || [] );
+ }
+
+ i = key.length;
+
+ while ( i-- ) {
+ delete cache[ key[ i ] ];
+ }
+ }
+
+ // Remove the expando if there's no more data
+ if ( key === undefined || jQuery.isEmptyObject( cache ) ) {
+
+ // Support: Chrome <=35 - 45
+ // Webkit & Blink performance suffers when deleting properties
+ // from DOM nodes, so set to undefined instead
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted)
+ if ( owner.nodeType ) {
+ owner[ this.expando ] = undefined;
+ } else {
+ delete owner[ this.expando ];
+ }
+ }
+ },
+ hasData: function( owner ) {
+ var cache = owner[ this.expando ];
+ return cache !== undefined && !jQuery.isEmptyObject( cache );
+ }
+};
+var dataPriv = new Data();
+
+var dataUser = new Data();
+
+
+
+// Implementation Summary
+//
+// 1. Enforce API surface and semantic compatibility with 1.9.x branch
+// 2. Improve the module's maintainability by reducing the storage
+// paths to a single mechanism.
+// 3. Use the same single mechanism to support "private" and "user" data.
+// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData)
+// 5. Avoid exposing implementation details on user objects (eg. expando properties)
+// 6. Provide a clear path for implementation upgrade to WeakMap in 2014
+
+var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,
+ rmultiDash = /[A-Z]/g;
+
+function getData( data ) {
+ if ( data === "true" ) {
+ return true;
+ }
+
+ if ( data === "false" ) {
+ return false;
+ }
+
+ if ( data === "null" ) {
+ return null;
+ }
+
+ // Only convert to a number if it doesn't change the string
+ if ( data === +data + "" ) {
+ return +data;
+ }
+
+ if ( rbrace.test( data ) ) {
+ return JSON.parse( data );
+ }
+
+ return data;
+}
+
+function dataAttr( elem, key, data ) {
+ var name;
+
+ // If nothing was found internally, try to fetch any
+ // data from the HTML5 data-* attribute
+ if ( data === undefined && elem.nodeType === 1 ) {
+ name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase();
+ data = elem.getAttribute( name );
+
+ if ( typeof data === "string" ) {
+ try {
+ data = getData( data );
+ } catch ( e ) {}
+
+ // Make sure we set the data so it isn't changed later
+ dataUser.set( elem, key, data );
+ } else {
+ data = undefined;
+ }
+ }
+ return data;
+}
+
+jQuery.extend( {
+ hasData: function( elem ) {
+ return dataUser.hasData( elem ) || dataPriv.hasData( elem );
+ },
+
+ data: function( elem, name, data ) {
+ return dataUser.access( elem, name, data );
+ },
+
+ removeData: function( elem, name ) {
+ dataUser.remove( elem, name );
+ },
+
+ // TODO: Now that all calls to _data and _removeData have been replaced
+ // with direct calls to dataPriv methods, these can be deprecated.
+ _data: function( elem, name, data ) {
+ return dataPriv.access( elem, name, data );
+ },
+
+ _removeData: function( elem, name ) {
+ dataPriv.remove( elem, name );
+ }
+} );
+
+jQuery.fn.extend( {
+ data: function( key, value ) {
+ var i, name, data,
+ elem = this[ 0 ],
+ attrs = elem && elem.attributes;
+
+ // Gets all values
+ if ( key === undefined ) {
+ if ( this.length ) {
+ data = dataUser.get( elem );
+
+ if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) {
+ i = attrs.length;
+ while ( i-- ) {
+
+ // Support: IE 11 only
+ // The attrs elements can be null (#14894)
+ if ( attrs[ i ] ) {
+ name = attrs[ i ].name;
+ if ( name.indexOf( "data-" ) === 0 ) {
+ name = jQuery.camelCase( name.slice( 5 ) );
+ dataAttr( elem, name, data[ name ] );
+ }
+ }
+ }
+ dataPriv.set( elem, "hasDataAttrs", true );
+ }
+ }
+
+ return data;
+ }
+
+ // Sets multiple values
+ if ( typeof key === "object" ) {
+ return this.each( function() {
+ dataUser.set( this, key );
+ } );
+ }
+
+ return access( this, function( value ) {
+ var data;
+
+ // The calling jQuery object (element matches) is not empty
+ // (and therefore has an element appears at this[ 0 ]) and the
+ // `value` parameter was not undefined. An empty jQuery object
+ // will result in `undefined` for elem = this[ 0 ] which will
+ // throw an exception if an attempt to read a data cache is made.
+ if ( elem && value === undefined ) {
+
+ // Attempt to get data from the cache
+ // The key will always be camelCased in Data
+ data = dataUser.get( elem, key );
+ if ( data !== undefined ) {
+ return data;
+ }
+
+ // Attempt to "discover" the data in
+ // HTML5 custom data-* attrs
+ data = dataAttr( elem, key );
+ if ( data !== undefined ) {
+ return data;
+ }
+
+ // We tried really hard, but the data doesn't exist.
+ return;
+ }
+
+ // Set the data...
+ this.each( function() {
+
+ // We always store the camelCased key
+ dataUser.set( this, key, value );
+ } );
+ }, null, value, arguments.length > 1, null, true );
+ },
+
+ removeData: function( key ) {
+ return this.each( function() {
+ dataUser.remove( this, key );
+ } );
+ }
+} );
+
+
+jQuery.extend( {
+ queue: function( elem, type, data ) {
+ var queue;
+
+ if ( elem ) {
+ type = ( type || "fx" ) + "queue";
+ queue = dataPriv.get( elem, type );
+
+ // Speed up dequeue by getting out quickly if this is just a lookup
+ if ( data ) {
+ if ( !queue || Array.isArray( data ) ) {
+ queue = dataPriv.access( elem, type, jQuery.makeArray( data ) );
+ } else {
+ queue.push( data );
+ }
+ }
+ return queue || [];
+ }
+ },
+
+ dequeue: function( elem, type ) {
+ type = type || "fx";
+
+ var queue = jQuery.queue( elem, type ),
+ startLength = queue.length,
+ fn = queue.shift(),
+ hooks = jQuery._queueHooks( elem, type ),
+ next = function() {
+ jQuery.dequeue( elem, type );
+ };
+
+ // If the fx queue is dequeued, always remove the progress sentinel
+ if ( fn === "inprogress" ) {
+ fn = queue.shift();
+ startLength--;
+ }
+
+ if ( fn ) {
+
+ // Add a progress sentinel to prevent the fx queue from being
+ // automatically dequeued
+ if ( type === "fx" ) {
+ queue.unshift( "inprogress" );
+ }
+
+ // Clear up the last queue stop function
+ delete hooks.stop;
+ fn.call( elem, next, hooks );
+ }
+
+ if ( !startLength && hooks ) {
+ hooks.empty.fire();
+ }
+ },
+
+ // Not public - generate a queueHooks object, or return the current one
+ _queueHooks: function( elem, type ) {
+ var key = type + "queueHooks";
+ return dataPriv.get( elem, key ) || dataPriv.access( elem, key, {
+ empty: jQuery.Callbacks( "once memory" ).add( function() {
+ dataPriv.remove( elem, [ type + "queue", key ] );
+ } )
+ } );
+ }
+} );
+
+jQuery.fn.extend( {
+ queue: function( type, data ) {
+ var setter = 2;
+
+ if ( typeof type !== "string" ) {
+ data = type;
+ type = "fx";
+ setter--;
+ }
+
+ if ( arguments.length < setter ) {
+ return jQuery.queue( this[ 0 ], type );
+ }
+
+ return data === undefined ?
+ this :
+ this.each( function() {
+ var queue = jQuery.queue( this, type, data );
+
+ // Ensure a hooks for this queue
+ jQuery._queueHooks( this, type );
+
+ if ( type === "fx" && queue[ 0 ] !== "inprogress" ) {
+ jQuery.dequeue( this, type );
+ }
+ } );
+ },
+ dequeue: function( type ) {
+ return this.each( function() {
+ jQuery.dequeue( this, type );
+ } );
+ },
+ clearQueue: function( type ) {
+ return this.queue( type || "fx", [] );
+ },
+
+ // Get a promise resolved when queues of a certain type
+ // are emptied (fx is the type by default)
+ promise: function( type, obj ) {
+ var tmp,
+ count = 1,
+ defer = jQuery.Deferred(),
+ elements = this,
+ i = this.length,
+ resolve = function() {
+ if ( !( --count ) ) {
+ defer.resolveWith( elements, [ elements ] );
+ }
+ };
+
+ if ( typeof type !== "string" ) {
+ obj = type;
+ type = undefined;
+ }
+ type = type || "fx";
+
+ while ( i-- ) {
+ tmp = dataPriv.get( elements[ i ], type + "queueHooks" );
+ if ( tmp && tmp.empty ) {
+ count++;
+ tmp.empty.add( resolve );
+ }
+ }
+ resolve();
+ return defer.promise( obj );
+ }
+} );
+var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source;
+
+var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" );
+
+
+var cssExpand = [ "Top", "Right", "Bottom", "Left" ];
+
+var isHiddenWithinTree = function( elem, el ) {
+
+ // isHiddenWithinTree might be called from jQuery#filter function;
+ // in that case, element will be second argument
+ elem = el || elem;
+
+ // Inline style trumps all
+ return elem.style.display === "none" ||
+ elem.style.display === "" &&
+
+ // Otherwise, check computed style
+ // Support: Firefox <=43 - 45
+ // Disconnected elements can have computed display: none, so first confirm that elem is
+ // in the document.
+ jQuery.contains( elem.ownerDocument, elem ) &&
+
+ jQuery.css( elem, "display" ) === "none";
+ };
+
+var swap = function( elem, options, callback, args ) {
+ var ret, name,
+ old = {};
+
+ // Remember the old values, and insert the new ones
+ for ( name in options ) {
+ old[ name ] = elem.style[ name ];
+ elem.style[ name ] = options[ name ];
+ }
+
+ ret = callback.apply( elem, args || [] );
+
+ // Revert the old values
+ for ( name in options ) {
+ elem.style[ name ] = old[ name ];
+ }
+
+ return ret;
+};
+
+
+
+
+function adjustCSS( elem, prop, valueParts, tween ) {
+ var adjusted,
+ scale = 1,
+ maxIterations = 20,
+ currentValue = tween ?
+ function() {
+ return tween.cur();
+ } :
+ function() {
+ return jQuery.css( elem, prop, "" );
+ },
+ initial = currentValue(),
+ unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ),
+
+ // Starting value computation is required for potential unit mismatches
+ initialInUnit = ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) &&
+ rcssNum.exec( jQuery.css( elem, prop ) );
+
+ if ( initialInUnit && initialInUnit[ 3 ] !== unit ) {
+
+ // Trust units reported by jQuery.css
+ unit = unit || initialInUnit[ 3 ];
+
+ // Make sure we update the tween properties later on
+ valueParts = valueParts || [];
+
+ // Iteratively approximate from a nonzero starting point
+ initialInUnit = +initial || 1;
+
+ do {
+
+ // If previous iteration zeroed out, double until we get *something*.
+ // Use string for doubling so we don't accidentally see scale as unchanged below
+ scale = scale || ".5";
+
+ // Adjust and apply
+ initialInUnit = initialInUnit / scale;
+ jQuery.style( elem, prop, initialInUnit + unit );
+
+ // Update scale, tolerating zero or NaN from tween.cur()
+ // Break the loop if scale is unchanged or perfect, or if we've just had enough.
+ } while (
+ scale !== ( scale = currentValue() / initial ) && scale !== 1 && --maxIterations
+ );
+ }
+
+ if ( valueParts ) {
+ initialInUnit = +initialInUnit || +initial || 0;
+
+ // Apply relative offset (+=/-=) if specified
+ adjusted = valueParts[ 1 ] ?
+ initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] :
+ +valueParts[ 2 ];
+ if ( tween ) {
+ tween.unit = unit;
+ tween.start = initialInUnit;
+ tween.end = adjusted;
+ }
+ }
+ return adjusted;
+}
+
+
+var defaultDisplayMap = {};
+
+function getDefaultDisplay( elem ) {
+ var temp,
+ doc = elem.ownerDocument,
+ nodeName = elem.nodeName,
+ display = defaultDisplayMap[ nodeName ];
+
+ if ( display ) {
+ return display;
+ }
+
+ temp = doc.body.appendChild( doc.createElement( nodeName ) );
+ display = jQuery.css( temp, "display" );
+
+ temp.parentNode.removeChild( temp );
+
+ if ( display === "none" ) {
+ display = "block";
+ }
+ defaultDisplayMap[ nodeName ] = display;
+
+ return display;
+}
+
+function showHide( elements, show ) {
+ var display, elem,
+ values = [],
+ index = 0,
+ length = elements.length;
+
+ // Determine new display value for elements that need to change
+ for ( ; index < length; index++ ) {
+ elem = elements[ index ];
+ if ( !elem.style ) {
+ continue;
+ }
+
+ display = elem.style.display;
+ if ( show ) {
+
+ // Since we force visibility upon cascade-hidden elements, an immediate (and slow)
+ // check is required in this first loop unless we have a nonempty display value (either
+ // inline or about-to-be-restored)
+ if ( display === "none" ) {
+ values[ index ] = dataPriv.get( elem, "display" ) || null;
+ if ( !values[ index ] ) {
+ elem.style.display = "";
+ }
+ }
+ if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) {
+ values[ index ] = getDefaultDisplay( elem );
+ }
+ } else {
+ if ( display !== "none" ) {
+ values[ index ] = "none";
+
+ // Remember what we're overwriting
+ dataPriv.set( elem, "display", display );
+ }
+ }
+ }
+
+ // Set the display of the elements in a second loop to avoid constant reflow
+ for ( index = 0; index < length; index++ ) {
+ if ( values[ index ] != null ) {
+ elements[ index ].style.display = values[ index ];
+ }
+ }
+
+ return elements;
+}
+
+jQuery.fn.extend( {
+ show: function() {
+ return showHide( this, true );
+ },
+ hide: function() {
+ return showHide( this );
+ },
+ toggle: function( state ) {
+ if ( typeof state === "boolean" ) {
+ return state ? this.show() : this.hide();
+ }
+
+ return this.each( function() {
+ if ( isHiddenWithinTree( this ) ) {
+ jQuery( this ).show();
+ } else {
+ jQuery( this ).hide();
+ }
+ } );
+ }
+} );
+var rcheckableType = ( /^(?:checkbox|radio)$/i );
+
+var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]+)/i );
+
+var rscriptType = ( /^$|\/(?:java|ecma)script/i );
+
+
+
+// We have to close these tags to support XHTML (#13200)
+var wrapMap = {
+
+ // Support: IE <=9 only
+ option: [ 1, "" ],
+
+ // XHTML parsers do not magically insert elements in the
+ // same way that tag soup parsers do. So we cannot shorten
+ // this by omitting or other required elements.
+ thead: [ 1, "
", "
" ],
+ col: [ 2, "
", "
" ],
+ tr: [ 2, "
", "
" ],
+ td: [ 3, "
", "
" ],
+
+ _default: [ 0, "", "" ]
+};
+
+// Support: IE <=9 only
+wrapMap.optgroup = wrapMap.option;
+
+wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
+wrapMap.th = wrapMap.td;
+
+
+function getAll( context, tag ) {
+
+ // Support: IE <=9 - 11 only
+ // Use typeof to avoid zero-argument method invocation on host objects (#15151)
+ var ret;
+
+ if ( typeof context.getElementsByTagName !== "undefined" ) {
+ ret = context.getElementsByTagName( tag || "*" );
+
+ } else if ( typeof context.querySelectorAll !== "undefined" ) {
+ ret = context.querySelectorAll( tag || "*" );
+
+ } else {
+ ret = [];
+ }
+
+ if ( tag === undefined || tag && nodeName( context, tag ) ) {
+ return jQuery.merge( [ context ], ret );
+ }
+
+ return ret;
+}
+
+
+// Mark scripts as having already been evaluated
+function setGlobalEval( elems, refElements ) {
+ var i = 0,
+ l = elems.length;
+
+ for ( ; i < l; i++ ) {
+ dataPriv.set(
+ elems[ i ],
+ "globalEval",
+ !refElements || dataPriv.get( refElements[ i ], "globalEval" )
+ );
+ }
+}
+
+
+var rhtml = /<|?\w+;/;
+
+function buildFragment( elems, context, scripts, selection, ignored ) {
+ var elem, tmp, tag, wrap, contains, j,
+ fragment = context.createDocumentFragment(),
+ nodes = [],
+ i = 0,
+ l = elems.length;
+
+ for ( ; i < l; i++ ) {
+ elem = elems[ i ];
+
+ if ( elem || elem === 0 ) {
+
+ // Add nodes directly
+ if ( jQuery.type( elem ) === "object" ) {
+
+ // Support: Android <=4.0 only, PhantomJS 1 only
+ // push.apply(_, arraylike) throws on ancient WebKit
+ jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );
+
+ // Convert non-html into a text node
+ } else if ( !rhtml.test( elem ) ) {
+ nodes.push( context.createTextNode( elem ) );
+
+ // Convert html into DOM nodes
+ } else {
+ tmp = tmp || fragment.appendChild( context.createElement( "div" ) );
+
+ // Deserialize a standard representation
+ tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase();
+ wrap = wrapMap[ tag ] || wrapMap._default;
+ tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ];
+
+ // Descend through wrappers to the right content
+ j = wrap[ 0 ];
+ while ( j-- ) {
+ tmp = tmp.lastChild;
+ }
+
+ // Support: Android <=4.0 only, PhantomJS 1 only
+ // push.apply(_, arraylike) throws on ancient WebKit
+ jQuery.merge( nodes, tmp.childNodes );
+
+ // Remember the top-level container
+ tmp = fragment.firstChild;
+
+ // Ensure the created nodes are orphaned (#12392)
+ tmp.textContent = "";
+ }
+ }
+ }
+
+ // Remove wrapper from fragment
+ fragment.textContent = "";
+
+ i = 0;
+ while ( ( elem = nodes[ i++ ] ) ) {
+
+ // Skip elements already in the context collection (trac-4087)
+ if ( selection && jQuery.inArray( elem, selection ) > -1 ) {
+ if ( ignored ) {
+ ignored.push( elem );
+ }
+ continue;
+ }
+
+ contains = jQuery.contains( elem.ownerDocument, elem );
+
+ // Append to fragment
+ tmp = getAll( fragment.appendChild( elem ), "script" );
+
+ // Preserve script evaluation history
+ if ( contains ) {
+ setGlobalEval( tmp );
+ }
+
+ // Capture executables
+ if ( scripts ) {
+ j = 0;
+ while ( ( elem = tmp[ j++ ] ) ) {
+ if ( rscriptType.test( elem.type || "" ) ) {
+ scripts.push( elem );
+ }
+ }
+ }
+ }
+
+ return fragment;
+}
+
+
+( function() {
+ var fragment = document.createDocumentFragment(),
+ div = fragment.appendChild( document.createElement( "div" ) ),
+ input = document.createElement( "input" );
+
+ // Support: Android 4.0 - 4.3 only
+ // Check state lost if the name is set (#11217)
+ // Support: Windows Web Apps (WWA)
+ // `name` and `type` must use .setAttribute for WWA (#14901)
+ input.setAttribute( "type", "radio" );
+ input.setAttribute( "checked", "checked" );
+ input.setAttribute( "name", "t" );
+
+ div.appendChild( input );
+
+ // Support: Android <=4.1 only
+ // Older WebKit doesn't clone checked state correctly in fragments
+ support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked;
+
+ // Support: IE <=11 only
+ // Make sure textarea (and checkbox) defaultValue is properly cloned
+ div.innerHTML = "";
+ support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue;
+} )();
+var documentElement = document.documentElement;
+
+
+
+var
+ rkeyEvent = /^key/,
+ rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/,
+ rtypenamespace = /^([^.]*)(?:\.(.+)|)/;
+
+function returnTrue() {
+ return true;
+}
+
+function returnFalse() {
+ return false;
+}
+
+// Support: IE <=9 only
+// See #13393 for more info
+function safeActiveElement() {
+ try {
+ return document.activeElement;
+ } catch ( err ) { }
+}
+
+function on( elem, types, selector, data, fn, one ) {
+ var origFn, type;
+
+ // Types can be a map of types/handlers
+ if ( typeof types === "object" ) {
+
+ // ( types-Object, selector, data )
+ if ( typeof selector !== "string" ) {
+
+ // ( types-Object, data )
+ data = data || selector;
+ selector = undefined;
+ }
+ for ( type in types ) {
+ on( elem, type, selector, data, types[ type ], one );
+ }
+ return elem;
+ }
+
+ if ( data == null && fn == null ) {
+
+ // ( types, fn )
+ fn = selector;
+ data = selector = undefined;
+ } else if ( fn == null ) {
+ if ( typeof selector === "string" ) {
+
+ // ( types, selector, fn )
+ fn = data;
+ data = undefined;
+ } else {
+
+ // ( types, data, fn )
+ fn = data;
+ data = selector;
+ selector = undefined;
+ }
+ }
+ if ( fn === false ) {
+ fn = returnFalse;
+ } else if ( !fn ) {
+ return elem;
+ }
+
+ if ( one === 1 ) {
+ origFn = fn;
+ fn = function( event ) {
+
+ // Can use an empty set, since event contains the info
+ jQuery().off( event );
+ return origFn.apply( this, arguments );
+ };
+
+ // Use same guid so caller can remove using origFn
+ fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
+ }
+ return elem.each( function() {
+ jQuery.event.add( this, types, fn, data, selector );
+ } );
+}
+
+/*
+ * Helper functions for managing events -- not part of the public interface.
+ * Props to Dean Edwards' addEvent library for many of the ideas.
+ */
+jQuery.event = {
+
+ global: {},
+
+ add: function( elem, types, handler, data, selector ) {
+
+ var handleObjIn, eventHandle, tmp,
+ events, t, handleObj,
+ special, handlers, type, namespaces, origType,
+ elemData = dataPriv.get( elem );
+
+ // Don't attach events to noData or text/comment nodes (but allow plain objects)
+ if ( !elemData ) {
+ return;
+ }
+
+ // Caller can pass in an object of custom data in lieu of the handler
+ if ( handler.handler ) {
+ handleObjIn = handler;
+ handler = handleObjIn.handler;
+ selector = handleObjIn.selector;
+ }
+
+ // Ensure that invalid selectors throw exceptions at attach time
+ // Evaluate against documentElement in case elem is a non-element node (e.g., document)
+ if ( selector ) {
+ jQuery.find.matchesSelector( documentElement, selector );
+ }
+
+ // Make sure that the handler has a unique ID, used to find/remove it later
+ if ( !handler.guid ) {
+ handler.guid = jQuery.guid++;
+ }
+
+ // Init the element's event structure and main handler, if this is the first
+ if ( !( events = elemData.events ) ) {
+ events = elemData.events = {};
+ }
+ if ( !( eventHandle = elemData.handle ) ) {
+ eventHandle = elemData.handle = function( e ) {
+
+ // Discard the second event of a jQuery.event.trigger() and
+ // when an event is called after a page has unloaded
+ return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ?
+ jQuery.event.dispatch.apply( elem, arguments ) : undefined;
+ };
+ }
+
+ // Handle multiple events separated by a space
+ types = ( types || "" ).match( rnothtmlwhite ) || [ "" ];
+ t = types.length;
+ while ( t-- ) {
+ tmp = rtypenamespace.exec( types[ t ] ) || [];
+ type = origType = tmp[ 1 ];
+ namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort();
+
+ // There *must* be a type, no attaching namespace-only handlers
+ if ( !type ) {
+ continue;
+ }
+
+ // If event changes its type, use the special event handlers for the changed type
+ special = jQuery.event.special[ type ] || {};
+
+ // If selector defined, determine special event api type, otherwise given type
+ type = ( selector ? special.delegateType : special.bindType ) || type;
+
+ // Update special based on newly reset type
+ special = jQuery.event.special[ type ] || {};
+
+ // handleObj is passed to all event handlers
+ handleObj = jQuery.extend( {
+ type: type,
+ origType: origType,
+ data: data,
+ handler: handler,
+ guid: handler.guid,
+ selector: selector,
+ needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
+ namespace: namespaces.join( "." )
+ }, handleObjIn );
+
+ // Init the event handler queue if we're the first
+ if ( !( handlers = events[ type ] ) ) {
+ handlers = events[ type ] = [];
+ handlers.delegateCount = 0;
+
+ // Only use addEventListener if the special events handler returns false
+ if ( !special.setup ||
+ special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
+
+ if ( elem.addEventListener ) {
+ elem.addEventListener( type, eventHandle );
+ }
+ }
+ }
+
+ if ( special.add ) {
+ special.add.call( elem, handleObj );
+
+ if ( !handleObj.handler.guid ) {
+ handleObj.handler.guid = handler.guid;
+ }
+ }
+
+ // Add to the element's handler list, delegates in front
+ if ( selector ) {
+ handlers.splice( handlers.delegateCount++, 0, handleObj );
+ } else {
+ handlers.push( handleObj );
+ }
+
+ // Keep track of which events have ever been used, for event optimization
+ jQuery.event.global[ type ] = true;
+ }
+
+ },
+
+ // Detach an event or set of events from an element
+ remove: function( elem, types, handler, selector, mappedTypes ) {
+
+ var j, origCount, tmp,
+ events, t, handleObj,
+ special, handlers, type, namespaces, origType,
+ elemData = dataPriv.hasData( elem ) && dataPriv.get( elem );
+
+ if ( !elemData || !( events = elemData.events ) ) {
+ return;
+ }
+
+ // Once for each type.namespace in types; type may be omitted
+ types = ( types || "" ).match( rnothtmlwhite ) || [ "" ];
+ t = types.length;
+ while ( t-- ) {
+ tmp = rtypenamespace.exec( types[ t ] ) || [];
+ type = origType = tmp[ 1 ];
+ namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort();
+
+ // Unbind all events (on this namespace, if provided) for the element
+ if ( !type ) {
+ for ( type in events ) {
+ jQuery.event.remove( elem, type + types[ t ], handler, selector, true );
+ }
+ continue;
+ }
+
+ special = jQuery.event.special[ type ] || {};
+ type = ( selector ? special.delegateType : special.bindType ) || type;
+ handlers = events[ type ] || [];
+ tmp = tmp[ 2 ] &&
+ new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" );
+
+ // Remove matching events
+ origCount = j = handlers.length;
+ while ( j-- ) {
+ handleObj = handlers[ j ];
+
+ if ( ( mappedTypes || origType === handleObj.origType ) &&
+ ( !handler || handler.guid === handleObj.guid ) &&
+ ( !tmp || tmp.test( handleObj.namespace ) ) &&
+ ( !selector || selector === handleObj.selector ||
+ selector === "**" && handleObj.selector ) ) {
+ handlers.splice( j, 1 );
+
+ if ( handleObj.selector ) {
+ handlers.delegateCount--;
+ }
+ if ( special.remove ) {
+ special.remove.call( elem, handleObj );
+ }
+ }
+ }
+
+ // Remove generic event handler if we removed something and no more handlers exist
+ // (avoids potential for endless recursion during removal of special event handlers)
+ if ( origCount && !handlers.length ) {
+ if ( !special.teardown ||
+ special.teardown.call( elem, namespaces, elemData.handle ) === false ) {
+
+ jQuery.removeEvent( elem, type, elemData.handle );
+ }
+
+ delete events[ type ];
+ }
+ }
+
+ // Remove data and the expando if it's no longer used
+ if ( jQuery.isEmptyObject( events ) ) {
+ dataPriv.remove( elem, "handle events" );
+ }
+ },
+
+ dispatch: function( nativeEvent ) {
+
+ // Make a writable jQuery.Event from the native event object
+ var event = jQuery.event.fix( nativeEvent );
+
+ var i, j, ret, matched, handleObj, handlerQueue,
+ args = new Array( arguments.length ),
+ handlers = ( dataPriv.get( this, "events" ) || {} )[ event.type ] || [],
+ special = jQuery.event.special[ event.type ] || {};
+
+ // Use the fix-ed jQuery.Event rather than the (read-only) native event
+ args[ 0 ] = event;
+
+ for ( i = 1; i < arguments.length; i++ ) {
+ args[ i ] = arguments[ i ];
+ }
+
+ event.delegateTarget = this;
+
+ // Call the preDispatch hook for the mapped type, and let it bail if desired
+ if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
+ return;
+ }
+
+ // Determine handlers
+ handlerQueue = jQuery.event.handlers.call( this, event, handlers );
+
+ // Run delegates first; they may want to stop propagation beneath us
+ i = 0;
+ while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) {
+ event.currentTarget = matched.elem;
+
+ j = 0;
+ while ( ( handleObj = matched.handlers[ j++ ] ) &&
+ !event.isImmediatePropagationStopped() ) {
+
+ // Triggered event must either 1) have no namespace, or 2) have namespace(s)
+ // a subset or equal to those in the bound event (both can have no namespace).
+ if ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) {
+
+ event.handleObj = handleObj;
+ event.data = handleObj.data;
+
+ ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle ||
+ handleObj.handler ).apply( matched.elem, args );
+
+ if ( ret !== undefined ) {
+ if ( ( event.result = ret ) === false ) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+ }
+ }
+ }
+
+ // Call the postDispatch hook for the mapped type
+ if ( special.postDispatch ) {
+ special.postDispatch.call( this, event );
+ }
+
+ return event.result;
+ },
+
+ handlers: function( event, handlers ) {
+ var i, handleObj, sel, matchedHandlers, matchedSelectors,
+ handlerQueue = [],
+ delegateCount = handlers.delegateCount,
+ cur = event.target;
+
+ // Find delegate handlers
+ if ( delegateCount &&
+
+ // Support: IE <=9
+ // Black-hole SVG