diff options
Diffstat (limited to 'tools/dochack')
-rw-r--r-- | tools/dochack/dochack.nim | 274 | ||||
-rw-r--r-- | tools/dochack/fuzzysearch.nim | 141 | ||||
-rw-r--r-- | tools/dochack/karax.nim | 343 |
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("<") + of '>': s.add(">") + 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.} |