dochack.nim 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. import dom
  2. import fuzzysearch
  3. import std/[jsfetch, asyncjs]
  4. proc setTheme(theme: cstring) {.exportc.} =
  5. document.documentElement.setAttribute("data-theme", theme)
  6. window.localStorage.setItem("theme", theme)
  7. # set `data-theme` attribute early to prevent white flash
  8. setTheme:
  9. let t = window.localStorage.getItem("theme")
  10. if t.isNil: cstring"auto" else: t
  11. proc onDOMLoaded(e: Event) {.exportc.} =
  12. # set theme select value
  13. document.getElementById("theme-select").value = window.localStorage.getItem("theme")
  14. for pragmaDots in document.getElementsByClassName("pragmadots"):
  15. pragmaDots.onclick = proc (event: Event) =
  16. # Hide tease
  17. event.target.parentNode.style.display = "none"
  18. # Show actual
  19. event.target.parentNode.nextSibling.style.display = "inline"
  20. proc tree(tag: cstring; kids: varargs[Element]): Element =
  21. result = document.createElement tag
  22. for k in kids:
  23. result.appendChild k
  24. proc add(parent, kid: Element) =
  25. if parent.nodeName == "TR" and (kid.nodeName == "TD" or kid.nodeName == "TH"):
  26. let k = document.createElement("TD")
  27. appendChild(k, kid)
  28. appendChild(parent, k)
  29. else:
  30. appendChild(parent, kid)
  31. proc setClass(e: Element; value: cstring) =
  32. e.setAttribute("class", value)
  33. proc text(s: cstring): Element = cast[Element](document.createTextNode(s))
  34. proc replaceById(id: cstring; newTree: Node) =
  35. let x = document.getElementById(id)
  36. x.parentNode.replaceChild(newTree, x)
  37. newTree.id = id
  38. proc clone(e: Element): Element {.importcpp: "#.cloneNode(true)", nodecl.}
  39. proc markElement(x: Element) {.importcpp: "#.__karaxMarker__ = true", nodecl.}
  40. proc isMarked(x: Element): bool {.
  41. importcpp: "#.hasOwnProperty('__karaxMarker__')", nodecl.}
  42. proc title(x: Element): cstring {.importcpp: "#.title", nodecl.}
  43. proc sort[T](x: var openArray[T]; cmp: proc(a, b: T): int) {.importcpp:
  44. "#.sort(#)", nodecl.}
  45. proc extractItems(x: Element; items: var seq[Element]) =
  46. if x == nil: return
  47. if x.nodeName == "A":
  48. items.add x
  49. else:
  50. for i in 0..<x.len:
  51. extractItems(x[i], items)
  52. # HTML trees are so shitty we transform the TOC into a decent
  53. # data-structure instead and work on that.
  54. type
  55. TocEntry = ref object
  56. heading: Element
  57. kids: seq[TocEntry]
  58. sortId: int
  59. doSort: bool
  60. proc extractItems(x: TocEntry; heading: cstring; items: var seq[Element]) =
  61. if x == nil: return
  62. if x.heading != nil and x.heading.textContent == heading:
  63. for i in 0..<x.kids.len:
  64. items.add x.kids[i].heading
  65. else:
  66. for k in x.kids:
  67. extractItems(k, heading, items)
  68. proc toHtml(x: TocEntry; isRoot=false): Element =
  69. if x == nil: return nil
  70. if x.kids.len == 0:
  71. if x.heading == nil: return nil
  72. return x.heading.clone
  73. result = tree("DIV")
  74. if x.heading != nil and not isMarked(x.heading):
  75. result.add x.heading.clone
  76. let ul = tree("UL")
  77. if isRoot:
  78. ul.setClass("simple simple-toc")
  79. else:
  80. ul.setClass("simple")
  81. if x.dosort:
  82. x.kids.sort(proc(a, b: TocEntry): int =
  83. if a.heading != nil and b.heading != nil:
  84. let x = a.heading.textContent
  85. let y = b.heading.textContent
  86. if x < y: return -1
  87. if x > y: return 1
  88. return 0
  89. else:
  90. # ensure sorting is stable:
  91. return a.sortId - b.sortId
  92. )
  93. for k in x.kids:
  94. let y = toHtml(k)
  95. if y != nil:
  96. ul.add tree("LI", y)
  97. if ul.len != 0: result.add ul
  98. if result.len == 0: result = nil
  99. proc isWhitespace(text: cstring): bool {.importcpp: r"!/\S/.test(#)".}
  100. proc isWhitespace(x: Element): bool =
  101. x.nodeName == "#text" and x.textContent.isWhitespace or x.nodeName == "#comment"
  102. proc toToc(x: Element; father: TocEntry) =
  103. if x.nodeName == "UL":
  104. let f = TocEntry(heading: nil, kids: @[], sortId: father.kids.len)
  105. var i = 0
  106. while i < x.len:
  107. var nxt = i+1
  108. while nxt < x.len and x[nxt].isWhitespace:
  109. inc nxt
  110. if nxt < x.len and x[i].nodeName == "LI" and x[i].len == 1 and
  111. x[nxt].nodeName == "UL":
  112. let e = TocEntry(heading: x[i][0], kids: @[], sortId: f.kids.len)
  113. let it = x[nxt]
  114. for j in 0..<it.len:
  115. toToc(it[j], e)
  116. f.kids.add e
  117. i = nxt+1
  118. else:
  119. toToc(x[i], f)
  120. inc i
  121. father.kids.add f
  122. elif isWhitespace(x):
  123. discard
  124. elif x.nodeName == "LI":
  125. var idx: seq[int] = @[]
  126. for i in 0 ..< x.len:
  127. if not x[i].isWhitespace: idx.add i
  128. if idx.len == 2 and x[idx[1]].nodeName == "UL":
  129. let e = TocEntry(heading: x[idx[0]], kids: @[], sortId: father.kids.len)
  130. let it = x[idx[1]]
  131. for j in 0..<it.len:
  132. toToc(it[j], e)
  133. father.kids.add e
  134. else:
  135. for i in 0..<x.len:
  136. toToc(x[i], father)
  137. else:
  138. father.kids.add TocEntry(heading: x, kids: @[], sortId: father.kids.len)
  139. proc tocul(x: Element): Element =
  140. # x is a 'ul' element
  141. result = tree("UL")
  142. for i in 0..<x.len:
  143. let it = x[i]
  144. if it.nodeName == "LI":
  145. result.add it.clone
  146. elif it.nodeName == "UL":
  147. result.add tocul(it)
  148. proc uncovered(x: TocEntry): TocEntry =
  149. if x.kids.len == 0 and x.heading != nil:
  150. return if not isMarked(x.heading): x else: nil
  151. result = TocEntry(heading: x.heading, kids: @[], sortId: x.sortId,
  152. doSort: x.doSort)
  153. for k in x.kids:
  154. let y = uncovered(k)
  155. if y != nil: result.kids.add y
  156. if result.kids.len == 0: result = nil
  157. proc mergeTocs(orig, news: TocEntry): TocEntry =
  158. result = uncovered(orig)
  159. if result == nil:
  160. result = news
  161. else:
  162. for i in 0..<news.kids.len:
  163. result.kids.add news.kids[i]
  164. proc buildToc(orig: TocEntry; types, procs: seq[Element]): TocEntry =
  165. var newStuff = TocEntry(heading: nil, kids: @[], doSort: true)
  166. for t in types:
  167. let c = TocEntry(heading: t.clone, kids: @[], doSort: true)
  168. t.markElement()
  169. for p in procs:
  170. if not isMarked(p):
  171. let xx = getElementsByClass(p.parentNode, "attachedType")
  172. if xx.len == 1 and xx[0].textContent == t.textContent:
  173. let q = tree("A", text(p.title))
  174. q.setAttr("href", p.getAttribute("href"))
  175. c.kids.add TocEntry(heading: q, kids: @[])
  176. p.markElement()
  177. newStuff.kids.add c
  178. result = mergeTocs(orig, newStuff)
  179. var alternative: Element
  180. proc togglevis(d: Element) =
  181. if d.style.display == "none":
  182. d.style.display = "inline"
  183. else:
  184. d.style.display = "none"
  185. proc groupBy*(value: cstring) {.exportc.} =
  186. let toc = document.getElementById("toc-list")
  187. if alternative.isNil:
  188. var tt = TocEntry(heading: nil, kids: @[])
  189. toToc(toc, tt)
  190. tt = tt.kids[0]
  191. var types: seq[Element] = @[]
  192. var procs: seq[Element] = @[]
  193. extractItems(tt, "Types", types)
  194. extractItems(tt, "Procs", procs)
  195. extractItems(tt, "Converters", procs)
  196. extractItems(tt, "Methods", procs)
  197. extractItems(tt, "Templates", procs)
  198. extractItems(tt, "Macros", procs)
  199. extractItems(tt, "Iterators", procs)
  200. let ntoc = buildToc(tt, types, procs)
  201. let x = toHtml(ntoc, isRoot=true)
  202. alternative = tree("DIV", x)
  203. if value == "type":
  204. replaceById("tocRoot", alternative)
  205. else:
  206. replaceById("tocRoot", tree("DIV"))
  207. togglevis(document.getElementById"toc-list")
  208. var
  209. db: seq[Node]
  210. contents: seq[cstring]
  211. proc escapeCString(x: var cstring) =
  212. # Original strings are already escaped except HTML tags, so
  213. # we only escape `<` and `>`.
  214. var s = ""
  215. for c in x:
  216. case c
  217. of '<': s.add("&lt;")
  218. of '>': s.add("&gt;")
  219. else: s.add(c)
  220. x = s.cstring
  221. proc dosearch(value: cstring): Element =
  222. if db.len == 0:
  223. return
  224. let ul = tree("UL")
  225. result = tree("DIV")
  226. result.setClass"search_results"
  227. var matches: seq[(Node, int)] = @[]
  228. for i in 0..<db.len:
  229. let c = contents[i]
  230. if c == "Examples" or c == "PEG construction":
  231. # Some manual exclusions.
  232. # Ideally these should be fixed in the index to be more
  233. # descriptive of what they are.
  234. continue
  235. let (score, matched) = fuzzymatch(value, c)
  236. if matched:
  237. matches.add((db[i], score))
  238. matches.sort(proc(a, b: auto): int = b[1] - a[1])
  239. for i in 0 ..< min(matches.len, 29):
  240. matches[i][0].innerHTML = matches[i][0].getAttribute("data-doc-search-tag")
  241. escapeCString(matches[i][0].innerHTML)
  242. ul.add(tree("LI", cast[Element](matches[i][0])))
  243. if ul.len == 0:
  244. result.add tree("B", text"no search results")
  245. else:
  246. result.add tree("B", text"search results")
  247. result.add ul
  248. proc loadIndex() {.async.} =
  249. ## Loads theindex.html to enable searching
  250. let
  251. indexURL = document.getElementById("indexLink").getAttribute("href")
  252. # Get root of project documentation by cutting off theindex.html from index href
  253. rootURL = ($indexURL)[0 ..< ^"theindex.html".len]
  254. var resp = fetch(indexURL).await().text().await()
  255. # Convert into element so we can use DOM functions to parse the html
  256. var indexElem = document.createElement("div")
  257. indexElem.innerHtml = resp
  258. # Add items into the DB/contents
  259. for href in indexElem.getElementsByClass("reference"):
  260. # Make links be relative to project root instead of current page
  261. href.setAttr("href", cstring(rootURL & $href.getAttribute("href")))
  262. db &= href
  263. contents &= href.getAttribute("data-doc-search-tag")
  264. var
  265. oldtoc: Element
  266. timer: Timeout
  267. loadIndexFut: Future[void] = nil
  268. proc search*() {.exportc.} =
  269. proc wrapper() =
  270. let elem = document.getElementById("searchInput")
  271. let value = elem.value
  272. if value.len != 0:
  273. if oldtoc.isNil:
  274. oldtoc = document.getElementById("tocRoot")
  275. let results = dosearch(value)
  276. replaceById("tocRoot", results)
  277. elif not oldtoc.isNil:
  278. replaceById("tocRoot", oldtoc)
  279. # Start loading index as soon as user starts typing.
  280. # Will only be loaded the once anyways
  281. if loadIndexFut == nil:
  282. loadIndexFut = loadIndex()
  283. # Run wrapper once loaded so we don't miss the users query
  284. discard loadIndexFut.then(wrapper)
  285. if timer != nil: clearTimeout(timer)
  286. timer = setTimeout(wrapper, 400)
  287. proc copyToClipboard*() {.exportc.} =
  288. {.emit: """
  289. function updatePreTags() {
  290. const allPreTags = document.querySelectorAll("pre:not(.line-nums)")
  291. allPreTags.forEach((e) => {
  292. const div = document.createElement("div")
  293. div.classList.add("copyToClipBoard")
  294. const preTag = document.createElement("pre")
  295. preTag.innerHTML = e.innerHTML
  296. const button = document.createElement("button")
  297. button.value = e.textContent.replace('...', '')
  298. button.classList.add("copyToClipBoardBtn")
  299. button.style.cursor = "pointer"
  300. div.appendChild(preTag)
  301. div.appendChild(button)
  302. e.outerHTML = div.outerHTML
  303. })
  304. }
  305. function copyTextToClipboard(e) {
  306. const clipBoardContent = e.target.value
  307. navigator.clipboard.writeText(clipBoardContent).then(function() {
  308. e.target.style.setProperty("--clipboard-image", "var(--clipboard-image-selected)")
  309. }, function(err) {
  310. console.error("Could not copy text: ", err);
  311. });
  312. }
  313. window.addEventListener("click", (e) => {
  314. if (e.target.classList.contains("copyToClipBoardBtn")) {
  315. copyTextToClipboard(e)
  316. }
  317. })
  318. window.addEventListener("mouseover", (e) => {
  319. if (e.target.nodeName === "PRE") {
  320. e.target.nextElementSibling.style.setProperty("--clipboard-image", "var(--clipboard-image-normal)")
  321. }
  322. })
  323. window.addEventListener("DOMContentLoaded", updatePreTags)
  324. """
  325. .}
  326. copyToClipboard()
  327. window.addEventListener("DOMContentLoaded", onDOMLoaded)