mirror of
https://github.com/nim-lang/Nim.git
synced 2025-12-29 17:34:43 +00:00
Adds a few fixes for when using code blocks with lines numbered - Use CSS variables for the colours so that it works in dark mode - Don't turn on normal table effects like hover and smaller font when its a line number table - With dochack.nim, don't add a clipboard copy button for the line numbers at the side [Example page showing the changes](https://66dcde6e4a655efb70771d9a--dazzling-kitten-6c3419.netlify.app/)
383 lines
11 KiB
Nim
383 lines
11 KiB
Nim
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))
|
|
|
|
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 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 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:
|
|
extractItems(x[i], 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 k in x.kids:
|
|
extractItems(k, 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 isWhitespace(text: cstring): bool {.importcpp: r"!/\S/.test(#)".}
|
|
|
|
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 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 k in x.kids:
|
|
let y = uncovered(k)
|
|
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 = getElementsByClass(p.parentNode, "attachedType")
|
|
if xx.len == 1 and xx[0].textContent == t.textContent:
|
|
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) =
|
|
if d.style.display == "none":
|
|
d.style.display = "inline"
|
|
else:
|
|
d.style.display = "none"
|
|
|
|
proc groupBy*(value: cstring) {.exportc.} =
|
|
let toc = document.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(document.getElementById"toc-list")
|
|
|
|
var
|
|
db: seq[Node]
|
|
contents: seq[cstring]
|
|
|
|
|
|
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.len == 0:
|
|
return
|
|
let ul = tree("UL")
|
|
result = tree("DIV")
|
|
result.setClass"search_results"
|
|
var matches: seq[(Node, int)] = @[]
|
|
for i in 0..<db.len:
|
|
let c = contents[i]
|
|
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
|
|
|
|
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 = document.getElementById("searchInput")
|
|
let value = elem.value
|
|
if value.len != 0:
|
|
if oldtoc.isNil:
|
|
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)
|