summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--compiler/docgen.nim6
-rw-r--r--config/nimdoc.cfg21
-rw-r--r--koch.nim1
-rw-r--r--tools/dochack/dochack.nim231
-rw-r--r--tools/dochack/karax.nim338
5 files changed, 596 insertions, 1 deletions
diff --git a/compiler/docgen.nim b/compiler/docgen.nim
index af940db00..22350a2bb 100644
--- a/compiler/docgen.nim
+++ b/compiler/docgen.nim
@@ -27,6 +27,7 @@ type
     seenSymbols: StringTableRef # avoids duplicate symbol generation for HTML.
     jArray: JsonNode
     types: TStrTable
+    isPureRst: bool
 
   PDoc* = ref TDocumentor ## Alias to type less.
 
@@ -634,7 +635,9 @@ proc genOutFile(d: PDoc): Rope =
     # Modules get an automatic title for the HTML, but no entry in the index.
     title = "Module " & extractFilename(changeFileExt(d.filename, ""))
 
-  let bodyname = if d.hasToc: "doc.body_toc" else: "doc.body_no_toc"
+  let bodyname = if d.hasToc and not d.isPureRst: "doc.body_toc_group"
+                 elif d.hasToc: "doc.body_toc"
+                 else: "doc.body_no_toc"
   content = ropeFormatNamedVars(getConfigVar(bodyname), ["title",
       "tableofcontents", "moduledesc", "date", "time", "content"],
       [title.rope, toc, d.modDesc, rope(getDateStr()),
@@ -699,6 +702,7 @@ proc commandDoc*() =
 proc commandRstAux(filename, outExt: string) =
   var filen = addFileExt(filename, "txt")
   var d = newDocumentor(filen, options.gConfigVars)
+  d.isPureRst = true
   var rst = parseRst(readFile(filen), filen, 0, 1, d.hasToc,
                      {roSupportRawDirective})
   var modDesc = newStringOfCap(30_000)
diff --git a/config/nimdoc.cfg b/config/nimdoc.cfg
index d19286703..2d6ce1861 100644
--- a/config/nimdoc.cfg
+++ b/config/nimdoc.cfg
@@ -83,6 +83,26 @@ doc.body_toc = """
 </div>
 """
 
+doc.body_toc_group = """
+<div class="row">
+  <div class="three columns">
+  <div>
+    Group by:
+    <select onchange="groupBy(this.value)">
+      <option value="section">Section</option>
+      <option value="type">Type</option>
+    </select>
+  </div>
+  $tableofcontents
+  </div>
+  <div class="nine columns" id="content">
+  <div id="tocRoot"></div>
+  <p class="module-desc">$moduledesc</p>
+  $content
+  </div>
+</div>
+"""
+
 doc.body_no_toc = """
 $moduledesc
 $content
@@ -1248,6 +1268,7 @@ dt pre > span.Operator ~ span.Identifier, dt pre > span.Operator ~ span.Operator
   }
 </style>
 
+<script type="text/javascript" src="../dochack.js"></script>
 
 <script type="text/javascript">
 function togglepragma(d) {
diff --git a/koch.nim b/koch.nim
index dadbc1f10..0565d01ef 100644
--- a/koch.nim
+++ b/koch.nim
@@ -200,6 +200,7 @@ proc install(args: string) =
   exec("sh ./install.sh $#" % args)
 
 proc web(args: string) =
+  exec("$# js tools/dochack/dochack.nim" % findNim())
   exec("$# cc -r tools/nimweb.nim $# web/website.ini --putenv:nimversion=$#" %
        [findNim(), args, VersionAsString])
 
diff --git a/tools/dochack/dochack.nim b/tools/dochack/dochack.nim
new file mode 100644
index 000000000..7e4ebb487
--- /dev/null
+++ b/tools/dochack/dochack.nim
@@ -0,0 +1,231 @@
+
+
+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 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.}
+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)
+
+# HTML trees are so shitty we transform the TOC into a decent
+# data-structure instead and work on that.
+type
+  TocEntry = ref object
+    heading: Element
+    kids: seq[TocEntry]
+    sortId: int
+    doSort: bool
+
+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)
+
+proc toHtml(x: TocEntry; isRoot=false): Element =
+  if x == nil: return nil
+  if x.kids.len == 0:
+    if x.heading == nil: return nil
+    return x.heading.clone
+  result = tree("DIV")
+  if x.heading != nil and not isMarked(x.heading):
+    result.add x.heading.clone
+  let ul = tree("UL")
+  if isRoot:
+    ul.setClass("simple simple-toc")
+  else:
+    ul.setClass("simple")
+  if x.dosort:
+    x.kids.sort(proc(a, b: TocEntry): int =
+      if a.heading != nil and b.heading != nil:
+        let x = a.heading.textContent
+        let y = b.heading.textContent
+        if x < y: return -1
+        if x > y: return 1
+        return 0
+      else:
+        # ensure sorting is stable:
+        return a.sortId - b.sortId
+    )
+  for k in x.kids:
+    let y = toHtml(k)
+    if y != nil:
+      ul.add tree("LI", y)
+  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(x: Element): bool =
+  x.nodeName == "#text" and x.textContent.isWhitespace or
+    x.nodeName == "#comment"
+
+proc toToc(x: Element; father: TocEntry) =
+  if x.nodeName == "UL":
+    let f = TocEntry(heading: nil, kids: @[], sortId: father.kids.len)
+    var i = 0
+    while i < x.len:
+      var nxt = i+1
+      while nxt < x.len and x[nxt].isWhitespace:
+        inc nxt
+      if nxt < x.len and x[i].nodeName == "LI" and x[i].len == 1 and
+          x[nxt].nodeName == "UL":
+        let e = TocEntry(heading: x[i][0], kids: @[], sortId: f.kids.len)
+        let it = x[nxt]
+        for j in 0..<it.len:
+          toToc(it[j], e)
+        f.kids.add e
+        i = nxt+1
+      else:
+        toToc(x[i], f)
+        inc i
+    father.kids.add f
+  elif isWhitespace(x):
+    discard
+  elif x.nodeName == "LI":
+    var idx: seq[int] = @[]
+    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 it = x[idx[1]]
+      for j in 0..<it.len:
+        toToc(it[j], e)
+      father.kids.add e
+    else:
+      for i in 0..<x.len:
+        toToc(x[i], father)
+  else:
+    father.kids.add TocEntry(heading: x, kids: @[],
+                             sortId: father.kids.len)
+
+proc tocul(x: Element): Element =
+  # x is a 'ul' element
+  result = tree("UL")
+  for i in 0..<x.len:
+    let it = x[i]
+    if it.nodeName == "LI":
+      result.add it.clone
+    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])
+    if y != nil: result.kids.add y
+  if result.kids.len == 0: result = nil
+
+proc mergeTocs(orig, news: TocEntry): TocEntry =
+  result = uncovered(orig)
+  if result == nil:
+    result = news
+  else:
+    for i in 0..<news.kids.len:
+      result.kids.add news.kids[i]
+
+proc buildToc(orig: TocEntry; types, procs: seq[Element]): TocEntry =
+  var newStuff = TocEntry(heading: nil, kids: @[], doSort: true)
+  for t in types:
+    let c = TocEntry(heading: t.clone, kids: @[], doSort: true)
+    t.markElement()
+    for p in procs:
+      if not isMarked(p):
+        let xx = karax.getElementsByClass(p.parent, cstring"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: @[])
+          p.markElement()
+    newStuff.kids.add c
+  result = mergeTocs(orig, newStuff)
+
+var alternative: Element
+
+proc togglevis(d: Element) =
+  asm """
+    if (`d`.style.display == 'none')
+      `d`.style.display = 'inline';
+    else
+      `d`.style.display = 'none';
+  """
+
+proc groupBy*(value: cstring) {.exportc.} =
+  let toc = getElementById("toc-list")
+  if alternative.isNil:
+    var tt = TocEntry(heading: nil, kids: @[])
+    toToc(toc, tt)
+    tt = tt.kids[0]
+
+    var types: seq[Element] = @[]
+    var procs: seq[Element] = @[]
+
+    extractItems(tt, "Types", types)
+    extractItems(tt, "Procs", procs)
+    extractItems(tt, "Converters", procs)
+    extractItems(tt, "Methods", procs)
+    extractItems(tt, "Templates", procs)
+    extractItems(tt, "Macros", procs)
+    extractItems(tt, "Iterators", procs)
+
+    let ntoc = buildToc(tt, types, procs)
+    let x = toHtml(ntoc, isRoot=true)
+    alternative = tree("DIV", x)
+  if value == "type":
+    replaceById("tocRoot", alternative)
+  else:
+    replaceById("tocRoot", tree("DIV"))
+  togglevis(getElementById"toc-list")
diff --git a/tools/dochack/karax.nim b/tools/dochack/karax.nim
new file mode 100644
index 000000000..d7d9a059a
--- /dev/null
+++ b/tools/dochack/karax.nim
@@ -0,0 +1,338 @@
+# 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.}
+
+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.}