deploy: 5cbc22f4a490dadddf2d9d85372b3b3cd53cb4c9

This commit is contained in:
falsycat
2023-02-23 09:37:53 +00:00
commit 167e9cb584
54 changed files with 1361 additions and 0 deletions

View File

@@ -0,0 +1 @@
const addCollapsibleCallouts=()=>{const e=document.querySelectorAll("blockquote.callout-collapsible");e.forEach(e=>e.addEventListener("click",e=>{e.currentTarget.classList.toggle("callout-collapsed")}))}

View File

@@ -0,0 +1 @@
const svgCopy='<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true"><path fill-rule="evenodd" d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"></path><path fill-rule="evenodd" d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"></path></svg>',svgCheck='<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true"><path fill-rule="evenodd" fill="rgb(63, 185, 80)" d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"></path></svg>',addCopyButtons=()=>{let e=document.getElementsByClassName("highlight");for(let t=0;t<e.length;t++)try{if(e[t].getElementsByClassName("clipboard-button").length)continue;let s=e[t].getElementsByTagName("code"),o=s[s.length-1];const n=document.createElement("button");n.className="clipboard-button",n.type="button",n.innerHTML=svgCopy,n.ariaLabel="opy the shown code",n.addEventListener("click",()=>{navigator.clipboard.writeText(o.innerText.replace(/\n\n/g,"\n")).then(()=>{n.blur(),n.innerHTML=svgCheck,setTimeout(()=>{n.innerHTML=svgCopy,n.style.borderColor=""},2e3)},e=>n.innerHTML="Error")});let i=e[t].getElementsByClassName("chroma")[0];e[t].insertBefore(n,i)}catch(e){console.debug(e)}}

View File

@@ -0,0 +1 @@
function addTitleToCodeBlocks(){const e=document.getElementsByClassName("highlight");for(let t=0;t<e.length;t++)try{if(e[t].title.length){let n=document.createElement("div");if(e[t].getElementsByClassName("code-title").length)continue;n.textContent=e[t].title,n.classList.add("code-title"),e[t].insertBefore(n,e[t].firstChild)}}catch(e){console.debug(e)}}

View File

@@ -0,0 +1 @@
const userPref=window.matchMedia("(prefers-color-scheme: light)").matches?"light":"dark",currentTheme=localStorage.getItem("theme")??userPref,syntaxTheme=document.querySelector("#theme-link");currentTheme&&(document.documentElement.setAttribute("saved-theme",currentTheme),syntaxTheme.href=currentTheme==="dark"?"https://falsy.cat/styles/_dark_syntax.bec558461529f0dd343a0b008c343934.min.css":"https://falsy.cat/styles/_light_syntax.86a48a52faebeaaf42158b72922b1c90.min.css");const switchTheme=e=>{e.target.checked?(document.documentElement.setAttribute("saved-theme","dark"),localStorage.setItem("theme","dark"),syntaxTheme.href="https://falsy.cat/styles/_dark_syntax.bec558461529f0dd343a0b008c343934.min.css"):(document.documentElement.setAttribute("saved-theme","light"),localStorage.setItem("theme","light"),syntaxTheme.href="https://falsy.cat/styles/_light_syntax.86a48a52faebeaaf42158b72922b1c90.min.css")};window.addEventListener("DOMContentLoaded",()=>{const e=document.querySelector("#darkmode-toggle");e.addEventListener("change",switchTheme,!1),currentTheme==="dark"&&(e.checked=!0)})

View File

@@ -0,0 +1 @@
(async function(){const t=e=>e.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/),n=new FlexSearch.Document({cache:!0,charset:"latin:extra",optimize:!0,index:[{field:"content",tokenize:"reverse",encode:t},{field:"title",tokenize:"forward",encode:t}]}),{content:e}=await fetchData;for(const[s,t]of Object.entries(e))n.add({id:s,title:t.title,content:removeMarkdown(t.content)});const s=t=>({id:t,url:t,title:e[t].title,content:e[t].content});registerHandlers(o=>{const e=o.target.value,i=n.search(e,[{field:"content",limit:10},{field:"title",limit:5}]),t=t=>{const e=i.filter(e=>e.field===t);return e.length===0?[]:[...e[0].result]},a=new Set([...t("title"),...t("content")]),r=[...a].map(s);displayResults(e,r,!0)})})()

View File

@@ -0,0 +1,279 @@
async function drawGraph(baseUrl, isHome, pathColors, graphConfig) {
let {
depth,
enableDrag,
enableLegend,
enableZoom,
opacityScale,
scale,
repelForce,
fontSize } = graphConfig;
const container = document.getElementById("graph-container")
const { index, links, content } = await fetchData
// Use .pathname to remove hashes / searchParams / text fragments
const cleanUrl = window.location.origin + window.location.pathname
const curPage = cleanUrl.replace(/\/$/g, "").replace(baseUrl, "")
const parseIdsFromLinks = (links) => [
...new Set(links.flatMap((link) => [link.source, link.target])),
]
// Links is mutated by d3. We want to use links later on, so we make a copy and pass that one to d3
// Note: shallow cloning does not work because it copies over references from the original array
const copyLinks = JSON.parse(JSON.stringify(links))
const neighbours = new Set()
const wl = [curPage || "/", "__SENTINEL"]
if (depth >= 0) {
while (depth >= 0 && wl.length > 0) {
// compute neighbours
const cur = wl.shift()
if (cur === "__SENTINEL") {
depth--
wl.push("__SENTINEL")
} else {
neighbours.add(cur)
const outgoing = index.links[cur] || []
const incoming = index.backlinks[cur] || []
wl.push(...outgoing.map((l) => l.target), ...incoming.map((l) => l.source))
}
}
} else {
parseIdsFromLinks(copyLinks).forEach((id) => neighbours.add(id))
}
const data = {
nodes: [...neighbours].map((id) => ({ id })),
links: copyLinks.filter((l) => neighbours.has(l.source) && neighbours.has(l.target)),
}
const color = (d) => {
if (d.id === curPage || (d.id === "/" && curPage === "")) {
return "var(--g-node-active)"
}
for (const pathColor of pathColors) {
const path = Object.keys(pathColor)[0]
const colour = pathColor[path]
if (d.id.startsWith(path)) {
return colour
}
}
return "var(--g-node)"
}
const drag = (simulation) => {
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(1).restart()
d.fx = d.x
d.fy = d.y
}
function dragged(event, d) {
d.fx = event.x
d.fy = event.y
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0)
d.fx = null
d.fy = null
}
const noop = () => { }
return d3
.drag()
.on("start", enableDrag ? dragstarted : noop)
.on("drag", enableDrag ? dragged : noop)
.on("end", enableDrag ? dragended : noop)
}
const height = Math.max(container.offsetHeight, isHome ? 500 : 250)
const width = container.offsetWidth
const simulation = d3
.forceSimulation(data.nodes)
.force("charge", d3.forceManyBody().strength(-100 * repelForce))
.force(
"link",
d3
.forceLink(data.links)
.id((d) => d.id)
.distance(40),
)
.force("center", d3.forceCenter())
const svg = d3
.select("#graph-container")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr('viewBox', [-width / 2 / scale, -height / 2 / scale, width / scale, height / scale])
if (enableLegend) {
const legend = [{ Current: "var(--g-node-active)" }, { Note: "var(--g-node)" }, ...pathColors]
legend.forEach((legendEntry, i) => {
const key = Object.keys(legendEntry)[0]
const colour = legendEntry[key]
svg
.append("circle")
.attr("cx", -width / 2 + 20)
.attr("cy", height / 2 - 30 * (i + 1))
.attr("r", 6)
.style("fill", colour)
svg
.append("text")
.attr("x", -width / 2 + 40)
.attr("y", height / 2 - 30 * (i + 1))
.text(key)
.style("font-size", "15px")
.attr("alignment-baseline", "middle")
})
}
// draw links between nodes
const link = svg
.append("g")
.selectAll("line")
.data(data.links)
.join("line")
.attr("class", "link")
.attr("stroke", "var(--g-link)")
.attr("stroke-width", 2)
.attr("data-source", (d) => d.source.id)
.attr("data-target", (d) => d.target.id)
// svg groups
const graphNode = svg.append("g").selectAll("g").data(data.nodes).enter().append("g")
// calculate radius
const nodeRadius = (d) => {
const numOut = index.links[d.id]?.length || 0
const numIn = index.backlinks[d.id]?.length || 0
return 2 + Math.sqrt(numOut + numIn)
}
// draw individual nodes
const node = graphNode
.append("circle")
.attr("class", "node")
.attr("id", (d) => d.id)
.attr("r", nodeRadius)
.attr("fill", color)
.style("cursor", "pointer")
.on("click", (_, d) => {
// SPA navigation
const targ = `${baseUrl}${decodeURI(d.id).replace(/\s+/g, "-")}/`
window.Million.navigate(new URL(targ), ".singlePage")
plausible("Link Click", {
props: {
href: targ,
broken: false,
internal: true,
graph: true,
}
})
})
.on("mouseover", function(_, d) {
d3.selectAll(".node").transition().duration(100).attr("fill", "var(--g-node-inactive)")
const neighbours = parseIdsFromLinks([
...(index.links[d.id] || []),
...(index.backlinks[d.id] || []),
])
const neighbourNodes = d3.selectAll(".node").filter((d) => neighbours.includes(d.id))
const currentId = d.id
window.Million.prefetch(new URL(`${baseUrl}${decodeURI(d.id).replace(/\s+/g, "-")}/`))
const linkNodes = d3
.selectAll(".link")
.filter((d) => d.source.id === currentId || d.target.id === currentId)
// highlight neighbour nodes
neighbourNodes.transition().duration(200).attr("fill", color)
// highlight links
linkNodes.transition().duration(200).attr("stroke", "var(--g-link-active)")
const bigFont = fontSize * 1.5
// show text for self
d3.select(this.parentNode)
.raise()
.select("text")
.transition()
.duration(200)
.attr('opacityOld', d3.select(this.parentNode).select('text').style("opacity"))
.style('opacity', 1)
.style('font-size', bigFont + 'em')
.attr('dy', d => nodeRadius(d) + 20 + 'px') // radius is in px
})
.on("mouseleave", function(_, d) {
d3.selectAll(".node").transition().duration(200).attr("fill", color)
const currentId = d.id
const linkNodes = d3
.selectAll(".link")
.filter((d) => d.source.id === currentId || d.target.id === currentId)
linkNodes.transition().duration(200).attr("stroke", "var(--g-link)")
d3.select(this.parentNode)
.select("text")
.transition()
.duration(200)
.style('opacity', d3.select(this.parentNode).select('text').attr("opacityOld"))
.style('font-size', fontSize + 'em')
.attr('dy', d => nodeRadius(d) + 8 + 'px') // radius is in px
})
.call(drag(simulation))
// draw labels
const labels = graphNode
.append("text")
.attr("dx", 0)
.attr("dy", (d) => nodeRadius(d) + 8 + "px")
.attr("text-anchor", "middle")
.text((d) => content[d.id]?.title || (d.id.charAt(1).toUpperCase() + d.id.slice(2)).replace("-", " "))
.style('opacity', (opacityScale - 1) / 3.75)
.style("pointer-events", "none")
.style('font-size', fontSize + 'em')
.raise()
.call(drag(simulation))
// set panning
if (enableZoom) {
svg.call(
d3
.zoom()
.extent([
[0, 0],
[width, height],
])
.scaleExtent([0.25, 4])
.on("zoom", ({ transform }) => {
link.attr("transform", transform)
node.attr("transform", transform)
const scale = transform.k * opacityScale;
const scaledOpacity = Math.max((scale - 1) / 3.75, 0)
labels.attr("transform", transform).style("opacity", scaledOpacity)
}),
)
}
// progress the simulation
simulation.on("tick", () => {
link
.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y)
node.attr("cx", (d) => d.x).attr("cy", (d) => d.y)
labels.attr("x", (d) => d.x).attr("y", (d) => d.y)
})
}

View File

@@ -0,0 +1,9 @@
function htmlToElement(e){const t=document.createElement("template");return e=e.trim(),t.innerHTML=e,t.content.firstChild}function initPopover(e,t){const n=e.replace(window.location.origin,"");fetchData.then(({content:e})=>{const s=[...document.getElementsByClassName("internal-link")];s.filter(e=>e.dataset.src||e.dataset.idx&&t).forEach(t=>{let s;if(t.dataset.ctx){const n=e[t.dataset.src],o=`<div class="popover">
<h3>${n.title}</h3>
<p>${highlight(removeMarkdown(n.content),t.dataset.ctx)}...</p>
<p class="meta">${new Date(n.lastmodified).toLocaleDateString()}</p>
</div>`;s=htmlToElement(o)}else{const o=e[t.dataset.src.replace(/\/$/g,"").replace(n,"")];if(o){let n=t.href.split("#"),e=removeMarkdown(o.content);if(n.length>1){let t=decodeURIComponent(n[1]).replace(/-/g," "),s=e.toLowerCase().indexOf("<b>"+t+"</b>");e=e.substring(s,e.length)}const i=`<div class="popover">
<h3>${o.title}</h3>
<p>${e.split(" ",20).join(" ")}...</p>
<p class="meta">${new Date(o.lastmodified).toLocaleDateString()}</p>
</div>`;s=htmlToElement(i)}}s&&(t.appendChild(s),LATEX_ENABLED&&renderMathInElement(s,{delimiters:[{left:"$$",right:"$$",display:!1},{left:"$",right:"$",display:!1}],throwOnError:!1}),t.addEventListener("mouseover",()=>{window.FloatingUIDOM.computePosition(t,s,{middleware:[window.FloatingUIDOM.offset(10),window.FloatingUIDOM.inline(),window.FloatingUIDOM.shift()]}).then(({x:e,y:t})=>{Object.assign(s.style,{left:`${e}px`,top:`${t}px`})}),s.classList.add("visible"),plausible("Popover Hover",{props:{href:t.dataset.src}})}),t.addEventListener("mouseout",()=>{s.classList.remove("visible")}))})})}

View File

@@ -0,0 +1 @@
import{apply,navigate,prefetch,router,}from"https://unpkg.com/million@1.11.5/dist/router.mjs";export const attachSPARouting=(e,n)=>{window.Million={apply,navigate,prefetch,router};const t=()=>requestAnimationFrame(n);window.addEventListener("DOMContentLoaded",()=>{apply(t=>e(t)),e(),router(".singlePage"),t()}),window.addEventListener("million:navigate",t)}

View File

@@ -0,0 +1,7 @@
const removeMarkdown=(n,t={listUnicodeChar:!1,stripListLeaders:!0,gfm:!0,useImgAltText:!1,preserveLinks:!1})=>{let e=n||"";e=e.replace(/^(-\s*?|\*\s*?|_\s*?){3,}\s*$/gm,"");try{t.stripListLeaders&&(t.listUnicodeChar?e=e.replace(/^([\s\t]*)([*\-+]|\d+\.)\s+/gm,t.listUnicodeChar+" $1"):e=e.replace(/^([\s\t]*)([*\-+]|\d+\.)\s+/gm,"$1")),t.gfm&&(e=e.replace(/\n={2,}/g,"\n").replace(/~{3}.*\n/g,"").replace(/~~/g,"").replace(/`{3}.*\n/g,"")),t.preserveLinks&&(e=e.replace(/\[(.*?)\][[(](.*?)[\])]/g,"$1 ($2)")),e=e.replace(/<[^>]*>/g,"").replace(/^[=-]{2,}\s*$/g,"").replace(/\[\^.+?\](: .*?$)?/g,"").replace(/(#{1,6})\s+(.+)\1?/g,"<b>$2</b>").replace(/\s{0,2}\[.*?\]: .*?$/g,"").replace(/!\[(.*?)\][[(].*?[\])]/g,t.useImgAltText?"$1":"").replace(/\[(.*?)\][[(].*?[\])]/g,"<a>$1</a>").replace(/!?\[\[\S[^[\]|]*(?:\|([^[\]]*))?\S\]\]/g,"<a>$1</a>").replace(/^\s{0,3}>\s?/g,"").replace(/(^|\n)\s{0,3}>\s?/g,"\n\n").replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g,"").replace(/([*_]{1,3})(\S.*?\S?)\1/g,"$2").replace(/([*_]{1,3})(\S.*?\S?)\1/g,"$2").replace(/(`{3,})(.*?)\1/gm,"$2").replace(/`(.+?)`/g,"$1").replace(/\n{2,}/g,"\n\n").replace(/\[![a-zA-Z]+\][-+]? /g,"")}catch(e){return console.error(e),n}return e},highlight=(e,n)=>{const t=20,o=e.indexOf(n);if(o!==-1){const s=t,i=e.substring(0,o).split(" ").slice(-s),a=e.substring(o+n.length,e.length-2).split(" ").slice(0,s);return(i.length===s?`...${i.join(" ")}`:i.join(" "))+`<span class="search-highlight">${n}</span>`+a.join(" ")}const u=n.split(/\s+/).filter(e=>e!==""),s=e.split(/\s+/).filter(e=>e!==""),a=e=>u.some(t=>e.toLowerCase().startsWith(t.toLowerCase())),r=s.map(a);let c=0,l=0;for(let e=0;e<Math.max(r.length-t,0);e++){const s=r.slice(e,e+t),n=s.reduce((e,t)=>e+t,0);n>=c&&(c=n,l=e)}const i=Math.max(l-t,0),d=Math.min(i+2*t,s.length),h=s.slice(i,d).map(e=>a(e)?`<span class="search-highlight">${e}</span>`:e).join(" ").replaceAll('</span> <span class="search-highlight">'," ");return`${i===0?"":"..."}${h}${d===s.length?"":"..."}`},resultToHTML=({url:e,title:t,content:n})=>`<button class="result-card" id="${e}">
<h3>${t}</h3>
<p>${n}</p>
</button>`,redir=(t,e)=>{const n=PRODUCTION&&SEARCH_ENABLED,s=n?"":BASE_URL.replace(/\/$/g,""),o=`${s}${t}#:~:text=${encodeURIComponent(e)}/`;window.Million.navigate(new URL(o),".singlePage"),closeSearch(),plausible("Search",{props:{term:e}})};function openSearch(){const t=document.getElementById("search-bar"),n=document.getElementById("results-container"),e=document.getElementById("search-container");e.style.display==="none"||e.style.display===""?(t.value="",n.innerHTML="",e.style.display="block",t.focus()):e.style.display="none"}function closeSearch(){const e=document.getElementById("search-container");e.style.display="none"}const registerHandlers=n=>{const e=document.getElementById("search-bar"),s=document.getElementById("search-container");let o;e.addEventListener("keyup",e=>{if(e.key==="Enter"){const e=document.getElementsByClassName("result-card")[0];redir(e.id,o)}}),e.addEventListener("input",n),document.addEventListener("keydown",e=>{e.key==="k"&&(e.ctrlKey||e.metaKey)&&(e.preventDefault(),openSearch()),e.key==="Escape"&&(e.preventDefault(),closeSearch())});const t=document.getElementById("search-icon");t.addEventListener("click",e=>{openSearch()}),t.addEventListener("keydown",e=>{openSearch()}),s.addEventListener("click",e=>{closeSearch()}),document.getElementById("search-space").addEventListener("click",e=>{e.stopPropagation()})},displayResults=(e,n,s=!1)=>{const t=document.getElementById("results-container");if(n.length===0)t.innerHTML=`<button class="result-card">
<h3>No results.</h3>
<p>Try another search term?</p>
</button>`;else{t.innerHTML=n.map(t=>s?resultToHTML({url:t.url,title:highlight(t.title,e),content:highlight(removeMarkdown(t.content),e)}):resultToHTML(t)).join("\n"),LATEX_ENABLED&&renderMathInElement(t,{delimiters:[{left:"$$",right:"$$",display:!1},{left:"$",right:"$",display:!1}],throwOnError:!1});const o=[...document.getElementsByClassName("result-card")];o.forEach(t=>{t.onclick=()=>redir(t.id,e)})}}