summary refs log tree commit diff stats
path: root/tools/dochack
diff options
context:
space:
mode:
Diffstat (limited to 'tools/dochack')
-rw-r--r--tools/dochack/dochack.nim274
-rw-r--r--tools/dochack/fuzzysearch.nim141
-rw-r--r--tools/dochack/karax.nim343
3 files changed, 321 insertions, 437 deletions
diff --git a/tools/dochack/dochack.nim b/tools/dochack/dochack.nim
index 79a0e7482..946945346 100644
--- a/tools/dochack/dochack.nim
+++ b/tools/dochack/dochack.nim
@@ -1,18 +1,52 @@
+import dom
+import fuzzysearch
+import std/[jsfetch, asyncjs]
+
+
+proc setTheme(theme: cstring) {.exportc.} =
+  document.documentElement.setAttribute("data-theme", theme)
+  window.localStorage.setItem("theme", theme)
+
+# set `data-theme` attribute early to prevent white flash
+setTheme:
+  let t = window.localStorage.getItem("theme")
+  if t.isNil: cstring"auto" else: t
+
+proc onDOMLoaded(e: Event) {.exportc.} =
+  # set theme select value
+  document.getElementById("theme-select").value = window.localStorage.getItem("theme")
+
+  for pragmaDots in document.getElementsByClassName("pragmadots"):
+    pragmaDots.onclick = proc (event: Event) =
+      # Hide tease
+      event.target.parentNode.style.display = "none"
+      # Show actual
+      event.target.parentNode.nextSibling.style.display = "inline"
+
+
+proc tree(tag: cstring; kids: varargs[Element]): Element =
+  result = document.createElement tag
+  for k in kids:
+    result.appendChild k
+
+proc add(parent, kid: Element) =
+  if parent.nodeName == "TR" and (kid.nodeName == "TD" or kid.nodeName == "TH"):
+    let k = document.createElement("TD")
+    appendChild(k, kid)
+    appendChild(parent, k)
+  else:
+    appendChild(parent, kid)
 
+proc setClass(e: Element; value: cstring) =
+  e.setAttribute("class", value)
+proc text(s: cstring): Element = cast[Element](document.createTextNode(s))
 
-import karax
-
-proc findNodeWith(x: Element; tag, content: cstring): Element =
-  if x.nodeName == tag and x.textContent == content:
-    return x
-  for i in 0..<x.len:
-    let it = x[i]
-    let y = findNodeWith(it, tag, content)
-    if y != nil: return y
-  return nil
+proc replaceById(id: cstring; newTree: Node) =
+  let x = document.getElementById(id)
+  x.parentNode.replaceChild(newTree, x)
+  newTree.id = id
 
 proc clone(e: Element): Element {.importcpp: "#.cloneNode(true)", nodecl.}
-proc parent(e: Element): Element {.importcpp: "#.parentNode", nodecl.}
 proc markElement(x: Element) {.importcpp: "#.__karaxMarker__ = true", nodecl.}
 proc isMarked(x: Element): bool {.
   importcpp: "#.hasOwnProperty('__karaxMarker__')", nodecl.}
@@ -21,20 +55,13 @@ proc title(x: Element): cstring {.importcpp: "#.title", nodecl.}
 proc sort[T](x: var openArray[T]; cmp: proc(a, b: T): int) {.importcpp:
   "#.sort(#)", nodecl.}
 
-proc parentWith(x: Element; tag: cstring): Element =
-  result = x.parent
-  while result.nodeName != tag:
-    result = result.parent
-    if result == nil: return nil
-
 proc extractItems(x: Element; items: var seq[Element]) =
   if x == nil: return
   if x.nodeName == "A":
     items.add x
   else:
     for i in 0..<x.len:
-      let it = x[i]
-      extractItems(it, items)
+      extractItems(x[i], items)
 
 # HTML trees are so shitty we transform the TOC into a decent
 # data-structure instead and work on that.
@@ -45,16 +72,14 @@ type
     sortId: int
     doSort: bool
 
-proc extractItems(x: TocEntry; heading: cstring;
-                  items: var seq[Element]) =
+proc extractItems(x: TocEntry; heading: cstring; items: var seq[Element]) =
   if x == nil: return
   if x.heading != nil and x.heading.textContent == heading:
     for i in 0..<x.kids.len:
       items.add x.kids[i].heading
   else:
-    for i in 0..<x.kids.len:
-      let it = x.kids[i]
-      extractItems(it, heading, items)
+    for k in x.kids:
+      extractItems(k, heading, items)
 
 proc toHtml(x: TocEntry; isRoot=false): Element =
   if x == nil: return nil
@@ -88,20 +113,10 @@ proc toHtml(x: TocEntry; isRoot=false): Element =
   if ul.len != 0: result.add ul
   if result.len == 0: result = nil
 
-proc containsWord(a, b: cstring): bool {.asmNoStackFrame.} =
-  {.emit: """
-     var escaped = `b`.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
-     return new RegExp("\\b" + escaped + "\\b").test(`a`);
-  """.}
-
-proc isWhitespace(text: cstring): bool {.asmNoStackFrame.} =
-  {.emit: """
-     return !/[^\s]/.test(`text`);
-  """.}
+proc isWhitespace(text: cstring): bool {.importcpp: r"!/\S/.test(#)".}
 
 proc isWhitespace(x: Element): bool =
-  x.nodeName == "#text" and x.textContent.isWhitespace or
-    x.nodeName == "#comment"
+  x.nodeName == "#text" and x.textContent.isWhitespace or x.nodeName == "#comment"
 
 proc toToc(x: Element; father: TocEntry) =
   if x.nodeName == "UL":
@@ -130,8 +145,7 @@ proc toToc(x: Element; father: TocEntry) =
     for i in 0 ..< x.len:
       if not x[i].isWhitespace: idx.add i
     if idx.len == 2 and x[idx[1]].nodeName == "UL":
-      let e = TocEntry(heading: x[idx[0]], kids: @[],
-                       sortId: father.kids.len)
+      let e = TocEntry(heading: x[idx[0]], kids: @[], sortId: father.kids.len)
       let it = x[idx[1]]
       for j in 0..<it.len:
         toToc(it[j], e)
@@ -140,8 +154,7 @@ proc toToc(x: Element; father: TocEntry) =
       for i in 0..<x.len:
         toToc(x[i], father)
   else:
-    father.kids.add TocEntry(heading: x, kids: @[],
-                             sortId: father.kids.len)
+    father.kids.add TocEntry(heading: x, kids: @[], sortId: father.kids.len)
 
 proc tocul(x: Element): Element =
   # x is a 'ul' element
@@ -153,18 +166,13 @@ proc tocul(x: Element): Element =
     elif it.nodeName == "UL":
       result.add tocul(it)
 
-proc getSection(toc: Element; name: cstring): Element =
-  let sec = findNodeWith(toc, "A", name)
-  if sec != nil:
-    result = sec.parentWith("LI")
-
 proc uncovered(x: TocEntry): TocEntry =
   if x.kids.len == 0 and x.heading != nil:
     return if not isMarked(x.heading): x else: nil
   result = TocEntry(heading: x.heading, kids: @[], sortId: x.sortId,
                     doSort: x.doSort)
-  for i in 0..<x.kids.len:
-    let y = uncovered(x.kids[i])
+  for k in x.kids:
+    let y = uncovered(k)
     if y != nil: result.kids.add y
   if result.kids.len == 0: result = nil
 
@@ -183,9 +191,8 @@ proc buildToc(orig: TocEntry; types, procs: seq[Element]): TocEntry =
     t.markElement()
     for p in procs:
       if not isMarked(p):
-        let xx = karax.getElementsByClass(p.parent, cstring"attachedType")
+        let xx = getElementsByClass(p.parentNode, "attachedType")
         if xx.len == 1 and xx[0].textContent == t.textContent:
-          #kout(cstring"found ", p.nodeName)
           let q = tree("A", text(p.title))
           q.setAttr("href", p.getAttribute("href"))
           c.kids.add TocEntry(heading: q, kids: @[])
@@ -196,15 +203,13 @@ proc buildToc(orig: TocEntry; types, procs: seq[Element]): TocEntry =
 var alternative: Element
 
 proc togglevis(d: Element) =
-  asm """
-    if (`d`.style.display == 'none')
-      `d`.style.display = 'inline';
-    else
-      `d`.style.display = 'none';
-  """
+  if d.style.display == "none":
+    d.style.display = "inline"
+  else:
+    d.style.display = "none"
 
 proc groupBy*(value: cstring) {.exportc.} =
-  let toc = getElementById("toc-list")
+  let toc = document.getElementById("toc-list")
   if alternative.isNil:
     var tt = TocEntry(heading: nil, kids: @[])
     toToc(toc, tt)
@@ -228,69 +233,150 @@ proc groupBy*(value: cstring) {.exportc.} =
     replaceById("tocRoot", alternative)
   else:
     replaceById("tocRoot", tree("DIV"))
-  togglevis(getElementById"toc-list")
+  togglevis(document.getElementById"toc-list")
 
 var
-  db: seq[Element]
+  db: seq[Node]
   contents: seq[cstring]
 
-template normalize(x: cstring): cstring = x.toLower.replace("_", "")
+
+proc escapeCString(x: var cstring) =
+  # Original strings are already escaped except HTML tags, so
+  # we only escape `<` and `>`.
+  var s = ""
+  for c in x:
+    case c
+    of '<': s.add("&lt;")
+    of '>': s.add("&gt;")
+    else: s.add(c)
+  x = s.cstring
 
 proc dosearch(value: cstring): Element =
-  if db.isNil:
-    var stuff: Element
-    {.emit: """
-    var request = new XMLHttpRequest();
-    request.open("GET", "theindex.html", false);
-    request.send(null);
-
-    var doc = document.implementation.createHTMLDocument("theindex");
-    doc.documentElement.innerHTML = request.responseText;
-
-    //parser=new DOMParser();
-    //doc=parser.parseFromString("<html></html>", "text/html");
-
-    `stuff` = doc.documentElement;
-    """.}
-    db = stuff.getElementsByClass"reference external"
-    contents = @[]
-    for ahref in db:
-      contents.add ahref.textContent.normalize
+  if db.len == 0:
+    return
   let ul = tree("UL")
   result = tree("DIV")
   result.setClass"search_results"
-  var matches: seq[(Element, int)] = @[]
-  let key = value.normalize
+  var matches: seq[(Node, int)] = @[]
   for i in 0..<db.len:
     let c = contents[i]
-    if c.containsWord(key):
-      matches.add((db[i], -(30_000 - c.len)))
-    elif c.contains(key):
-      matches.add((db[i], c.len))
-  matches.sort do (a, b: auto) -> int:
-    a[1] - b[1]
-  for i in 0..min(<matches.len, 19):
-    ul.add(tree("LI", matches[i][0]))
+    if c == "Examples" or c == "PEG construction":
+    # Some manual exclusions.
+    # Ideally these should be fixed in the index to be more
+    # descriptive of what they are.
+      continue
+    let (score, matched) = fuzzymatch(value, c)
+    if matched:
+      matches.add((db[i], score))
+
+  matches.sort(proc(a, b: auto): int = b[1] - a[1])
+  for i in 0 ..< min(matches.len, 29):
+    matches[i][0].innerHTML = matches[i][0].getAttribute("data-doc-search-tag")
+    escapeCString(matches[i][0].innerHTML)
+    ul.add(tree("LI", cast[Element](matches[i][0])))
   if ul.len == 0:
     result.add tree("B", text"no search results")
   else:
     result.add tree("B", text"search results")
     result.add ul
 
-var oldtoc: Element
-var timer: Timeout
+proc loadIndex() {.async.} =
+  ## Loads theindex.html to enable searching
+  let
+    indexURL = document.getElementById("indexLink").getAttribute("href")
+    # Get root of project documentation by cutting off theindex.html from index href
+    rootURL = ($indexURL)[0 ..< ^"theindex.html".len]
+  var resp = fetch(indexURL).await().text().await()
+  # Convert into element so we can use DOM functions to parse the html
+  var indexElem = document.createElement("div")
+  indexElem.innerHtml = resp
+  # Add items into the DB/contents
+  for href in indexElem.getElementsByClass("reference"):
+    # Make links be relative to project root instead of current page
+    href.setAttr("href", cstring(rootURL & $href.getAttribute("href")))
+    db &= href
+    contents &= href.getAttribute("data-doc-search-tag")
+
+
+var
+  oldtoc: Element
+  timer: Timeout
+  loadIndexFut: Future[void] = nil
 
 proc search*() {.exportc.} =
   proc wrapper() =
-    let elem = getElementById("searchInput")
+    let elem = document.getElementById("searchInput")
     let value = elem.value
-    if value != "":
+    if value.len != 0:
       if oldtoc.isNil:
-        oldtoc = getElementById("tocRoot")
+        oldtoc = document.getElementById("tocRoot")
       let results = dosearch(value)
       replaceById("tocRoot", results)
     elif not oldtoc.isNil:
       replaceById("tocRoot", oldtoc)
-
+  # Start loading index as soon as user starts typing.
+  # Will only be loaded the once anyways
+  if loadIndexFut == nil:
+    loadIndexFut = loadIndex()
+    # Run wrapper once loaded so we don't miss the users query
+    discard loadIndexFut.then(wrapper)
   if timer != nil: clearTimeout(timer)
   timer = setTimeout(wrapper, 400)
+
+proc copyToClipboard*() {.exportc.} =
+    {.emit: """
+
+    function updatePreTags() {
+
+      const allPreTags = document.querySelectorAll("pre:not(.line-nums)")
+
+      allPreTags.forEach((e) => {
+      
+          const div = document.createElement("div")
+          div.classList.add("copyToClipBoard")
+    
+          const preTag = document.createElement("pre")
+          preTag.innerHTML = e.innerHTML
+    
+          const button = document.createElement("button")
+          button.value = e.textContent.replace('...', '') 
+          button.classList.add("copyToClipBoardBtn")
+          button.style.cursor = "pointer"
+    
+          div.appendChild(preTag)
+          div.appendChild(button)
+    
+          e.outerHTML = div.outerHTML
+      
+      })
+    }
+
+
+    function copyTextToClipboard(e) {
+        const clipBoardContent = e.target.value
+        navigator.clipboard.writeText(clipBoardContent).then(function() {
+            e.target.style.setProperty("--clipboard-image", "var(--clipboard-image-selected)")
+        }, function(err) {
+            console.error("Could not copy text: ", err);
+        });
+    }
+
+    window.addEventListener("click", (e) => {
+        if (e.target.classList.contains("copyToClipBoardBtn")) {
+            copyTextToClipboard(e)
+          }
+    })
+
+    window.addEventListener("mouseover", (e) => {
+        if (e.target.nodeName === "PRE") {
+            e.target.nextElementSibling.style.setProperty("--clipboard-image", "var(--clipboard-image-normal)")
+        }
+    })
+    
+    window.addEventListener("DOMContentLoaded", updatePreTags)
+
+    """
+    .}
+
+copyToClipboard()
+window.addEventListener("DOMContentLoaded", onDOMLoaded)
diff --git a/tools/dochack/fuzzysearch.nim b/tools/dochack/fuzzysearch.nim
new file mode 100644
index 000000000..fecbaf4f0
--- /dev/null
+++ b/tools/dochack/fuzzysearch.nim
@@ -0,0 +1,141 @@
+# A Fuzzy Match implementation inspired by the sublime text fuzzy match algorithm
+# as described here: https://blog.forrestthewoods.com/reverse-engineering-sublime-text-s-fuzzy-match-4cffeed33fdb
+# Heavily modified to provide more subjectively useful results
+# for on the Nim manual.
+#
+import strutils
+import math
+
+
+const
+  MaxUnmatchedLeadingChar = 3
+  ## Maximum number of times the penalty for unmatched leading chars is applied.
+
+  HeadingScaleFactor = 0.5
+  ## The score from before the colon Char is multiplied by this.
+  ## This is to weight function signatures and descriptions over module titles.
+
+
+type
+  ScoreCard = enum
+    StartMatch           = -100 ## Start matching.
+    LeadingCharDiff      = -3   ## An unmatched, leading character was found.
+    CharDiff             = -1   ## An unmatched character was found.
+    CharMatch            = 0    ## A matched character was found.
+    ConsecutiveMatch     = 5    ## A consecutive match was found.
+    LeadingCharMatch     = 10   ## The character matches the beginning of the
+                                ## string or the first character of a word
+                                ## or camel case boundary.
+    WordBoundryMatch     = 20   ## The last ConsecutiveCharMatch that
+                                ## immediately precedes the end of the string,
+                                ## end of the pattern, or a LeadingCharMatch.
+
+
+proc fuzzyMatch*(pattern, str: cstring) : tuple[score: int, matched: bool] =
+  var
+    scoreState = StartMatch
+    headerMatched = false
+    unmatchedLeadingCharCount = 0
+    consecutiveMatchCount = 0
+    strIndex = 0
+    patIndex = 0
+    score = 0
+
+  template transition(nextState) =
+    scoreState = nextState
+    score += ord(scoreState)
+
+  while (strIndex < str.len) and (patIndex < pattern.len):
+    var
+      patternChar = pattern[patIndex].toLowerAscii
+      strChar     = str[strIndex].toLowerAscii
+
+    # Ignore certain characters
+    if patternChar in {'_', ' ', '.'}:
+      patIndex += 1
+      continue
+    if strChar in {'_', ' ', '.'}:
+      strIndex += 1
+      continue
+
+    # Since this algorithm will be used to search against Nim documentation,
+    # the below logic prioritizes headers.
+    if not headerMatched and strChar == ':':
+      headerMatched = true
+      scoreState = StartMatch
+      score = int(floor(HeadingScaleFactor * float(score)))
+      patIndex = 0
+      strIndex += 1
+      continue
+
+    if strChar == patternChar:
+      case scoreState
+      of StartMatch, WordBoundryMatch:
+        scoreState = LeadingCharMatch
+
+      of CharMatch:
+        transition(ConsecutiveMatch)
+
+      of LeadingCharMatch, ConsecutiveMatch:
+        consecutiveMatchCount += 1
+        scoreState = ConsecutiveMatch
+        score += ord(ConsecutiveMatch) * consecutiveMatchCount
+
+        if scoreState == LeadingCharMatch:
+          score += ord(LeadingCharMatch)
+
+        var onBoundary = (patIndex == high(pattern))
+        if not onBoundary and strIndex < high(str):
+          let
+            nextPatternChar = toLowerAscii(pattern[patIndex + 1])
+            nextStrChar     = toLowerAscii(str[strIndex + 1])
+
+          onBoundary = (
+            nextStrChar notin {'a'..'z'} and
+            nextStrChar != nextPatternChar
+          )
+
+        if onBoundary:
+          transition(WordBoundryMatch)
+
+      of CharDiff, LeadingCharDiff:
+        var isLeadingChar = (
+          str[strIndex - 1] notin Letters or
+          str[strIndex - 1] in {'a'..'z'} and
+          str[strIndex] in {'A'..'Z'}
+        )
+
+        if isLeadingChar:
+          scoreState = LeadingCharMatch
+          #a non alpha or a camel case transition counts as a leading char.
+          # Transition the state, but don't give the bonus yet; wait until we verify a consecutive match.
+        else:
+          transition(CharMatch)
+      patIndex += 1
+
+    else:
+      case scoreState
+      of StartMatch:
+        transition(LeadingCharDiff)
+
+      of ConsecutiveMatch:
+        transition(CharDiff)
+        consecutiveMatchCount = 0
+
+      of LeadingCharDiff:
+        if unmatchedLeadingCharCount < MaxUnmatchedLeadingChar:
+          transition(LeadingCharDiff)
+        unmatchedLeadingCharCount += 1
+
+      else:
+        transition(CharDiff)
+
+    strIndex += 1
+
+  if patIndex == pattern.len and (strIndex == str.len or str[strIndex] notin Letters):
+    score += 10
+
+  result = (
+    score:   max(0, score),
+    matched: (score > 0),
+  )
diff --git a/tools/dochack/karax.nim b/tools/dochack/karax.nim
deleted file mode 100644
index d9619992b..000000000
--- a/tools/dochack/karax.nim
+++ /dev/null
@@ -1,343 +0,0 @@
-# Simple lib to write JS UIs
-
-import dom
-
-export dom.Element, dom.Event, dom.cloneNode, dom
-
-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 className*(e: Node): cstring {.importcpp: "#.className", nodecl.}
-proc `className=`*(e: Node; v: cstring) {.importcpp: "#.className = #", nodecl.}
-
-proc value*(e: Element): cstring {.importcpp: "#.value", nodecl.}
-proc `value=`*(e: Element; v: cstring) {.importcpp: "#.value = #", nodecl.}
-
-proc getElementsByClass*(e: Element; name: cstring): seq[Element] {.importcpp: "#.getElementsByClassName(#)", nodecl.}
-
-proc toLower*(x: cstring): cstring {.
-  importcpp: "#.toLowerCase()", nodecl.}
-proc replace*(x: cstring; search, by: cstring): cstring {.
-  importcpp: "#.replace(#, #)", nodecl.}
-
-type
-  EventHandler* = proc(ev: Event)
-  EventHandlerId* = proc(ev: Event; id: int)
-
-  Timeout* = ref object
-
-var document* {.importc.}: Document
-
-var
-  dorender: proc (): Element {.closure.}
-  drawTimeout: Timeout
-  currentTree: Element
-
-proc setRenderer*(renderer: proc (): Element) =
-  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: Element): cstring {.
-  importcpp: "#.textContent", nodecl.}
-
-proc replaceById*(id: cstring; newTree: Node) =
-  let x = getElementById(id)
-  x.parentNode.replaceChild(newTree, x)
-  newTree.id = id
-
-proc equals(a, b: Node): bool =
-  if a.nodeType != b.nodeType: return false
-  if a.id != b.id: return false
-  if a.nodeName != b.nodeName: return false
-  if a.nodeType == TextNode:
-    if a.data != b.data: return false
-  elif a.childNodes.len != b.childNodes.len:
-    return false
-  if a.className != b.className:
-    # style differences are updated in place and we pretend
-    # it's still the same node
-    a.className = b.className
-    #return false
-  return true
-
-proc diffTree(parent, a, b: Node) =
-  if equals(a, b):
-    if a.nodeType != TextNode:
-      # we need to do this correctly in the presence of asyncronous
-      # DOM updates:
-      var i = 0
-      while i < a.childNodes.len and a.childNodes.len == b.childNodes.len:
-        diffTree(a, a.childNodes[i], b.childNodes[i])
-        inc i
-  elif parent == nil:
-    replaceById("ROOT", b)
-  else:
-    parent.replaceChild(b, a)
-
-proc dodraw() =
-  let newtree = dorender()
-  newtree.id = "ROOT"
-  if currentTree == nil:
-    currentTree = newtree
-    replaceById("ROOT", currentTree)
-  else:
-    diffTree(nil, currentTree, newtree)
-
-proc redraw*() =
-  # we buffer redraw requests:
-  if drawTimeout != nil:
-    clearTimeout(drawTimeout)
-  drawTimeout = setTimeout(dodraw, 30)
-
-proc tree*(tag: string; kids: varargs[Element]): Element =
-  result = document.createElement tag
-  for k in kids:
-    result.appendChild k
-
-proc tree*(tag: string; attrs: openarray[(string, string)];
-           kids: varargs[Element]): Element =
-  result = tree(tag, kids)
-  for a in attrs: result.setAttribute(a[0], a[1])
-
-proc text*(s: string): Element = cast[Element](document.createTextNode(s))
-proc text*(s: cstring): Element = cast[Element](document.createTextNode(s))
-proc add*(parent, kid: Element) =
-  if parent.nodeName == "TR" and (kid.nodeName == "TD" or kid.nodeName == "TH"):
-    let k = document.createElement("TD")
-    appendChild(k, kid)
-    appendChild(parent, k)
-  else:
-    appendChild(parent, kid)
-
-proc len*(x: Element): int {.importcpp: "#.childNodes.length".}
-proc `[]`*(x: Element; idx: int): Element {.importcpp: "#.childNodes[#]".}
-
-proc isInt*(s: cstring): bool {.asmNoStackFrame.} =
-  asm """
-    return s.match(/^[0-9]+$/);
-  """
-
-var
-  linkCounter: int
-
-proc link*(id: int): Element =
-  result = document.createElement("a")
-  result.setAttribute("href", "#")
-  inc linkCounter
-  result.setAttribute("id", $linkCounter & ":" & $id)
-
-proc link*(action: EventHandler): Element =
-  result = document.createElement("a")
-  result.setAttribute("href", "#")
-  addEventListener(result, "click", action)
-
-proc parseInt*(s: cstring): int {.importc, nodecl.}
-proc parseFloat*(s: cstring): float {.importc, nodecl.}
-proc split*(s, sep: cstring): seq[cstring] {.importcpp, nodecl.}
-
-proc startsWith*(a, b: cstring): bool {.importcpp: "startsWith", nodecl.}
-proc contains*(a, b: cstring): bool {.importcpp: "(#.indexOf(#)>=0)", nodecl.}
-proc substr*(s: cstring; start: int): cstring {.importcpp: "substr", nodecl.}
-proc substr*(s: cstring; start, length: int): cstring {.importcpp: "substr", nodecl.}
-
-#proc len*(s: cstring): int {.importcpp: "#.length", nodecl.}
-proc `&`*(a, b: cstring): cstring {.importcpp: "(# + #)", nodecl.}
-proc toCstr*(s: int): cstring {.importcpp: "((#)+'')", nodecl.}
-
-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 valueAsInt*(e: Element): int = parseInt(e.value)
-proc suffixAsInt*(s, prefix: cstring): int = parseInt(suffix(s, prefix))
-
-proc scrollTop*(e: Element): int {.importcpp: "#.scrollTop", nodecl.}
-proc offsetHeight*(e: Element): int {.importcpp: "#.offsetHeight", nodecl.}
-proc offsetTop*(e: Element): int {.importcpp: "#.offsetTop", nodecl.}
-
-template onImpl(s) {.dirty} =
-  proc wrapper(ev: Event) =
-    action(ev)
-    redraw()
-  addEventListener(e, s, wrapper)
-
-proc setOnclick*(e: Element; action: proc(ev: Event)) =
-  onImpl "click"
-
-proc setOnclick*(e: Element; action: proc(ev: Event; id: int)) =
-  proc wrapper(ev: Event) =
-    let id = ev.target.id
-    let a = id.split(":")
-    if a.len == 2:
-      action(ev, parseInt(a[1]))
-      redraw()
-    else:
-      kout(cstring("cannot deal with id "), id)
-  addEventListener(e, "click", wrapper)
-
-proc setOnfocuslost*(e: Element; action: EventHandler) =
-  onImpl "blur"
-
-proc setOnchanged*(e: Element; action: EventHandler) =
-  onImpl "change"
-
-proc setOnscroll*(e: Element; action: EventHandler) =
-  onImpl "scroll"
-
-proc select*(choices: openarray[string]): Element =
-  result = document.createElement("select")
-  var i = 0
-  for c in choices:
-    result.add tree("option", [("value", $i)], text(c))
-    inc i
-
-proc select*(choices: openarray[(int, string)]): Element =
-  result = document.createElement("select")
-  for c in choices:
-    result.add tree("option", [("value", $c[0])], text(c[1]))
-
-var radioCounter: int
-
-proc radio*(choices: openarray[(int, string)]): Element =
-  result = document.createElement("fieldset")
-  var i = 0
-  inc radioCounter
-  for c in choices:
-    let id = "radio_" & c[1] & $i
-    var kid = tree("input", [("type", "radio"),
-      ("id", id), ("name", "radio" & $radioCounter),
-      ("value", $c[0])])
-    if i == 0:
-      kid.setAttribute("checked", "checked")
-    var lab = tree("label", [("for", id)], text(c[1]))
-    kid.add lab
-    result.add kid
-    inc i
-
-proc tag*(name: string; id="", class=""): Element =
-  result = document.createElement(name)
-  if id.len > 0:
-    result.setAttribute("id", id)
-  if class.len > 0:
-    result.setAttribute("class", class)
-
-proc tdiv*(id="", class=""): Element = tag("div", id, class)
-proc span*(id="", class=""): Element = tag("span", id, class)
-
-proc th*(s: string): Element =
-  result = tag("th")
-  result.add text(s)
-
-proc td*(s: string): Element =
-  result = tag("td")
-  result.add text(s)
-
-proc td*(s: Element): Element =
-  result = tag("td")
-  result.add s
-
-proc td*(class: string; s: Element): Element =
-  result = tag("td")
-  result.add s
-  result.setAttribute("class", class)
-
-proc table*(class="", kids: varargs[Element]): Element =
-  result = tag("table", "", class)
-  for k in kids: result.add k
-
-proc tr*(kids: varargs[Element]): Element =
-  result = tag("tr")
-  for k in kids:
-    if k.nodeName == "TD" or k.nodeName == "TH":
-      result.add k
-    else:
-      result.add td(k)
-
-proc setClass*(e: Element; value: string) =
-  e.setAttribute("class", value)
-
-proc setAttr*(e: Element; key, value: cstring) =
-  e.setAttribute(key, value)
-
-proc getAttr*(e: Element; key: cstring): cstring {.
-  importcpp: "#.getAttribute(#)", nodecl.}
-
-proc realtimeInput*(id, val: string; changed: proc(value: cstring)): Element =
-  let oldElem = getElementById(id)
-  #if oldElem != nil: return oldElem
-  let newVal = if oldElem.isNil: val else: $oldElem.value
-  var timer: Timeout
-  proc wrapper() =
-    changed(getElementById(id).value)
-    redraw()
-  proc onkeyup(ev: Event) =
-    if timer != nil: clearTimeout(timer)
-    timer = setTimeout(wrapper, 400)
-  result = tree("input", [("type", "text"),
-    ("value", newVal),
-    ("id", id)])
-  result.addEventListener("keyup", onkeyup)
-
-proc ajax(meth, url: cstring; headers: openarray[(string, string)];
-          data: cstring;
-          cont: proc (httpStatus: int; response: cstring)) =
-  proc setRequestHeader(a, b: cstring) {.importc: "ajax.setRequestHeader".}
-  {.emit: """
-  var ajax = new XMLHttpRequest();
-  ajax.open(`meth`,`url`,true);""".}
-  for a, b in items(headers):
-    setRequestHeader(a, b)
-  {.emit: """
-  ajax.onreadystatechange = function(){
-    if(this.readyState == 4){
-      if(this.status == 200){
-        `cont`(this.status, this.responseText);
-      } else {
-        `cont`(this.status, this.statusText);
-      }
-    }
-  }
-  ajax.send(`data`);
-  """.}
-
-proc ajaxPut*(url: string; headers: openarray[(string, string)];
-          data: cstring;
-          cont: proc (httpStatus: int, response: cstring)) =
-  ajax("PUT", url, headers, data, cont)
-
-proc ajaxGet*(url: string; headers: openarray[(string, string)];
-          cont: proc (httpStatus: int, response: cstring)) =
-  ajax("GET", url, headers, nil, cont)
-
-{.push stackTrace:off.}
-
-proc setupErrorHandler*(useAlert=false) =
-  ## Installs an error handler that transforms native JS unhandled
-  ## exceptions into Nim based stack traces. If `useAlert` is false,
-  ## the error message it put into the console, otherwise `alert`
-  ## is called.
-  proc stackTraceAsCstring(): cstring = cstring(getStackTrace())
-  {.emit: """
-  window.onerror = function(msg, url, line, col, error) {
-    var x = "Error: " + msg + "\n" + `stackTraceAsCstring`()
-    if (`useAlert`)
-      alert(x);
-    else
-      console.log(x);
-    var suppressErrorAlert = true;
-    return suppressErrorAlert;
-  };""".}
-
-{.pop.}