diff options
-rw-r--r-- | compiler/docgen.nim | 6 | ||||
-rw-r--r-- | config/nimdoc.cfg | 21 | ||||
-rw-r--r-- | koch.nim | 1 | ||||
-rw-r--r-- | tools/dochack/dochack.nim | 231 | ||||
-rw-r--r-- | tools/dochack/karax.nim | 338 |
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.} |