diff --git a/assets/js/graph.js b/assets/js/graph.js new file mode 100644 index 000000000..174d4946d --- /dev/null +++ b/assets/js/graph.js @@ -0,0 +1,270 @@ +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 * 1 / scale, -height / 2 * 1 / scale, width * 1 / scale, height * 1 / 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 3 + (numOut + numIn) / 4 + } + + // 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 + window.Million.navigate(new URL(`${baseUrl}${decodeURI(d.id).replace(/\s+/g, "-")}/`), ".singlePage") + }) + .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.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) + }) +} diff --git a/assets/js/router.js b/assets/js/router.js new file mode 100644 index 000000000..3bdc81023 --- /dev/null +++ b/assets/js/router.js @@ -0,0 +1,26 @@ +import { + apply, + navigate, + prefetch, + router, +} from "https://unpkg.com/million@1.9.8-0/dist/router.mjs" + +export const attachSPARouting = (init, rerender) => { + // Attach SPA functions to the global Million namespace + window.Million = { + apply, + navigate, + prefetch, + router, + } + + const render = () => requestAnimationFrame(rerender) + + window.addEventListener("DOMContentLoaded", () => { + apply((doc) => init(doc)) + init() + router(".singlePage") + render() + }) + window.addEventListener("million:navigate", render) +} diff --git a/assets/js/search.js b/assets/js/search.js new file mode 100644 index 000000000..5896061ba --- /dev/null +++ b/assets/js/search.js @@ -0,0 +1,261 @@ +// code from https://github.com/danestves/markdown-to-text +const removeMarkdown = ( + markdown, + options = { + listUnicodeChar: false, + stripListLeaders: true, + gfm: true, + useImgAltText: false, + preserveLinks: false, + }, +) => { + let output = markdown || "" + output = output.replace(/^(-\s*?|\*\s*?|_\s*?){3,}\s*$/gm, "") + + try { + if (options.stripListLeaders) { + if (options.listUnicodeChar) + output = output.replace(/^([\s\t]*)([\*\-\+]|\d+\.)\s+/gm, options.listUnicodeChar + " $1") + else output = output.replace(/^([\s\t]*)([\*\-\+]|\d+\.)\s+/gm, "$1") + } + if (options.gfm) { + output = output + .replace(/\n={2,}/g, "\n") + .replace(/~{3}.*\n/g, "") + .replace(/~~/g, "") + .replace(/`{3}.*\n/g, "") + } + if (options.preserveLinks) { + output = output.replace(/\[(.*?)\][\[\(](.*?)[\]\)]/g, "$1 ($2)") + } + output = output + .replace(/<[^>]*>/g, "") + .replace(/^[=\-]{2,}\s*$/g, "") + .replace(/\[\^.+?\](\: .*?$)?/g, "") + .replace(/(#{1,6})\s+(.+)\1?/g, "$2") + .replace(/\s{0,2}\[.*?\]: .*?$/g, "") + .replace(/\!\[(.*?)\][\[\(].*?[\]\)]/g, options.useImgAltText ? "$1" : "") + .replace(/\[(.*?)\][\[\(].*?[\]\)]/g, "$1") + .replace(/!?\[\[\S[^\[\]\|]*(?:\|([^\[\]]*))?\S\]\]/g, "$1") + .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{0,1})\1/g, "$2") + .replace(/([\*_]{1,3})(\S.*?\S{0,1})\1/g, "$2") + .replace(/(`{3,})(.*?)\1/gm, "$2") + .replace(/`(.+?)`/g, "$1") + .replace(/\n{2,}/g, "\n\n") + } catch (e) { + console.error(e) + return markdown + } + return output +} +// ----- + +const highlight = (content, term) => { + const highlightWindow = 20 + + // try to find direct match first + const directMatchIdx = content.indexOf(term) + if (directMatchIdx !== -1) { + const h = highlightWindow / 2 + const before = content.substring(0, directMatchIdx).split(" ").slice(-h) + const after = content + .substring(directMatchIdx + term.length, content.length - 1) + .split(" ") + .slice(0, h) + return ( + (before.length == h ? `...${before.join(" ")}` : before.join(" ")) + + `${term}` + + after.join(" ") + ) + } + + const tokenizedTerm = term.split(/\s+/).filter((t) => t !== "") + const splitText = content.split(/\s+/).filter((t) => t !== "") + const includesCheck = (token) => + tokenizedTerm.some((term) => token.toLowerCase().startsWith(term.toLowerCase())) + + const occurrencesIndices = splitText.map(includesCheck) + + // calculate best index + let bestSum = 0 + let bestIndex = 0 + for (let i = 0; i < Math.max(occurrencesIndices.length - highlightWindow, 0); i++) { + const window = occurrencesIndices.slice(i, i + highlightWindow) + const windowSum = window.reduce((total, cur) => total + cur, 0) + if (windowSum >= bestSum) { + bestSum = windowSum + bestIndex = i + } + } + + const startIndex = Math.max(bestIndex - highlightWindow, 0) + const endIndex = Math.min(startIndex + 2 * highlightWindow, splitText.length) + const mappedText = splitText + .slice(startIndex, endIndex) + .map((token) => { + if (includesCheck(token)) { + return `${token}` + } + return token + }) + .join(" ") + .replaceAll(' ', " ") + return `${startIndex === 0 ? "" : "..."}${mappedText}${ + endIndex === splitText.length ? "" : "..." + }` +} + +;(async function () { + const encoder = (str) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])+/) + const contentIndex = new FlexSearch.Document({ + cache: true, + charset: "latin:extra", + optimize: true, + index: [ + { + field: "content", + tokenize: "reverse", + encode: encoder, + }, + { + field: "title", + tokenize: "forward", + encode: encoder, + }, + ], + }) + + const { content } = await fetchData + for (const [key, value] of Object.entries(content)) { + contentIndex.add({ + id: key, + title: value.title, + content: removeMarkdown(value.content), + }) + } + + const resultToHTML = ({ url, title, content, term }) => { + const text = removeMarkdown(content) + const resultTitle = highlight(title, term) + const resultText = highlight(text, term) + return `` + } + + const redir = (id, term) => { + // SPA navigation + window.Million.navigate( + new URL(`${BASE_URL.replace(/\/$/g, "")}${id}#:~:text=${encodeURIComponent(term)}/`), + ".singlePage", + ) + closeSearch() + } + + const formatForDisplay = (id) => ({ + id, + url: id, + title: content[id].title, + content: content[id].content, + }) + + const source = document.getElementById("search-bar") + const results = document.getElementById("results-container") + let term + source.addEventListener("keyup", (e) => { + if (e.key === "Enter") { + const anchor = document.getElementsByClassName("result-card")[0] + redir(anchor.id, term) + } + }) + source.addEventListener("input", (e) => { + term = e.target.value + const searchResults = contentIndex.search(term, [ + { + field: "content", + limit: 10, + }, + { + field: "title", + limit: 5, + }, + ]) + const getByField = (field) => { + const results = searchResults.filter((x) => x.field === field) + if (results.length === 0) { + return [] + } else { + return [...results[0].result] + } + } + const allIds = new Set([...getByField("title"), ...getByField("content")]) + const finalResults = [...allIds].map(formatForDisplay) + + // display + if (finalResults.length === 0) { + results.innerHTML = `` + } else { + results.innerHTML = finalResults + .map((result) => + resultToHTML({ + ...result, + term, + }), + ) + .join("\n") + const anchors = [...document.getElementsByClassName("result-card")] + anchors.forEach((anchor) => { + anchor.onclick = () => redir(anchor.id, term) + }) + } + }) + + const searchContainer = document.getElementById("search-container") + + function openSearch() { + if (searchContainer.style.display === "none" || searchContainer.style.display === "") { + source.value = "" + results.innerHTML = "" + searchContainer.style.display = "block" + source.focus() + } else { + searchContainer.style.display = "none" + } + } + + function closeSearch() { + searchContainer.style.display = "none" + } + + document.addEventListener("keydown", (event) => { + if (event.key === "k" && (event.ctrlKey || event.metaKey)) { + event.preventDefault() + openSearch() + } + if (event.key === "Escape") { + event.preventDefault() + closeSearch() + } + }) + + const searchButton = document.getElementById("search-icon") + searchButton.addEventListener("click", (evt) => { + openSearch() + }) + searchButton.addEventListener("keydown", (evt) => { + openSearch() + }) + searchContainer.addEventListener("click", (evt) => { + closeSearch() + }) + document.getElementById("search-space").addEventListener("click", (evt) => { + evt.stopPropagation() + }) +})() diff --git a/assets/styles/base.scss b/assets/styles/base.scss new file mode 100644 index 000000000..1c353f338 --- /dev/null +++ b/assets/styles/base.scss @@ -0,0 +1,595 @@ +:root { + --lt-colours-light: var(--light) !important; + --lt-colours-lightgray: var(--lightgray) !important; + --lt-colours-dark: var(--secondary) !important; + --lt-colours-secondary: var(--tertiary) !important; + --lt-colours-gray: var(--outlinegray) !important; +} + +h1, h2, h3, h4, h5, h6, ol, ul, thead { + font-family: Inter; + color: var(--dark); + font-weight: revert; + margin: revert; + padding: revert; + + &:hover > .hanchor { + opacity: 1; + } +} + +.hanchor { + font-family: Inter; + margin-left: -1em; + opacity: 0.3; + transition: opacity 0.3s ease; + color: var(--secondary); + +} + +p, ul, text { + font-family: 'Source Sans Pro', sans-serif; + color: var(--gray); + fill: var(--gray); + font-weight: revert; + margin: revert; + padding: revert; +} + +.mainTOC { + background: var(--lightgray); + border-radius: 5px; + padding: 0.75em 1em; +} + +.mainTOC details summary { + cursor: zoom-in; + font-family: Inter; + color: var(--dark); + font-weight: 700; +} + +.mainTOC details[open] summary { + cursor: zoom-out; +} + +#TableOfContents > ol { + counter-reset: section; + margin-left: 0em; + padding-left: 1.5em; + & > li { + counter-increment: section; + & > ol { + counter-reset: subsection; + & > li { + counter-increment: subsection; + &::marker { + content: counter(section) "." counter(subsection) " "; + } + } + } + } + + & > li::marker { + content: counter(section) " "; + } + + & > li::marker, & > li > ol > li::marker { + font-family: Source Sans Pro; + font-weight: 700; + } +} + +table { + width: 100%; +} + +img { + width: 100%; + border-radius: 3px; + margin: 1em 0; +} + +p>img+em { + display: block; + transform: translateY(-1em); +} + +sup { + line-height: 0 +} + +p, tbody, li { + font-family: Source Sans Pro; + color: var(--gray); + line-height: 1.5em; +} + +blockquote { + margin-left: 0em; + border-left: 3px solid var(--secondary); + padding-left: 1em; + transition: border-color 0.2s ease; + + &:hover { + border-color: var(--tertiary); + } +} + +table { + padding: 1.5em; +} + +td, th { + padding: 0.1em 0.5em; +} + +.footnotes p { + margin: 0.5em 0; +} + +.pagination { + list-style: none; + padding-left: 0; + display: flex; + margin-top: 2em; + gap: 1.5em; + justify-content: center; + + .disabled { + opacity: 0.2; + } + + & > li { + text-align: center; + display: inline-block; + + & a { + background-color: transparent !important; + } + + & a[href$="#"] { + opacity: 0.2; + } + } +} + +.section { + & h3 > a { + font-weight: 700; + font-family: Inter; + margin: 0; + } + & p { + margin-top: 0; + } +} + +article { + & > .meta { + margin: -1.5em 0 1em 0; + opacity: 0.7; + } + + & a { + font-family: Source Sans Pro; + font-weight: 600; + + &.internal-link { + text-decoration: none; + background-color: transparentize(#8f9fa9, 0.85); + padding: 0 0.1em; + margin: auto -0.1em; + border-radius: 3px; + + &.broken { + opacity: 0.5; + background-color: transparent; + } + } + } + + & p { + overflow-wrap: anywhere; + } +} + +.tags { + list-style: none; + padding-left: 0; + + & .meta { + & > h1 { + margin: 0; + } + & > p { + margin: 0; + } + } + + & > li { + display: inline-block; + margin: 0.4em 0; + } + + & > li > a { + border-radius: 8px; + border: var(--outlinegray) 1px solid; + padding: 0.2em 0.5em; + &::before { + content: "#"; + margin-right: 0.3em; + color: var(--outlinegray); + } + } +} + +.backlinks a { + font-weight: 600; + font-size: 0.9rem; +} + +sup > a { + text-decoration: none; + padding: 0 0.1em 0 0.2em; +} + +a { + font-family: Inter, sans-serif; + font-size: 1em; + font-weight: 700; + text-decoration: none; + transition: all 0.2s ease; + color: var(--secondary); + + &:hover { + color: var(--tertiary) !important; + } +} + +pre { + font-family: 'Fira Code'; + padding: 0.75em; + border-radius: 3px; + overflow-x: scroll; +} + +code { + font-family: 'Fira Code'; + font-size: 0.85em; + padding: 0.15em 0.3em; + border-radius: 5px; + background: var(--lightgray); +} + +html { + scroll-behavior: smooth; + + &:lang(ar) { + & p, & h1, & h2, & h3, article { + direction: rtl; + text-align: right; + } + } +} + +body { + margin: 0; + height: 100vh; + width: 100vw; + //overflow-x: hidden; + max-width: 100%; + box-sizing: border-box; + background-color: var(--light); +} + +@keyframes fadeIn { + 0% {opacity:0;} + 100% {opacity:1;} +} + +footer { + margin-top: 4em; + text-align: center; + & ul { + padding-left: 0; + } +} + +hr { + width: 25%; + margin: 4em auto; + height: 2px; + border-radius: 1px; + border-width: 0; + color: var(--dark); + background-color: var(--dark); +} + +.singlePage { + padding: 4em 30vw; + + @media all and (max-width: 1200px) { + padding: 25px 5vw; + } +} + +.page-end { + display: flex; + flex-direction: row; + gap: 2em; + + @media all and (max-width: 780px) { + flex-direction: column; + } + + & > * { + flex: 1 0 0; + } + + & > .backlinks-container { + & > ul { + list-style: none; + padding-left: 0; + + & > li { + margin: 0.5em 0; + padding: 0.25em 1em; + border: var(--outlinegray) 1px solid; + border-radius: 5px + } + } + } + + & #graph-container { + border: var(--outlinegray) 1px solid; + border-radius: 5px; + box-sizing: border-box; + min-height: 250px; + + & > svg { + margin-bottom: -5px; + + } + } +} + +.centered { + margin-top: 30vh; +} + +article > h1 { + font-size: 2em; +} + +header { + display: flex; + flex-direction: row; + align-items: center; + + & > h1 { + font-size: 2em; + } + + & > nav { + @media all and (max-width: 600px) { + display: none; + } + } + + & > .spacer { + flex: 1 1 auto; + } + + & > svg { + cursor: pointer; + width: 18px; + min-width: 18px; + margin: 0 1em; + + &:hover .search-path { + stroke: var(--tertiary); + } + + .search-path { + stroke: var(--gray); + stroke-width: 2px; + transition: stroke 0.5s ease; + } + } +} + +#search-container { + position: fixed; + z-index: 9999; + left: 0; + top: 0; + width: 100vw; + height: 100%; + overflow: scroll; + display: none; + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + + & > div { + width: 50%; + margin-top: 15vh; + margin-left: auto; + margin-right: auto; + + @media all and (max-width: 1200px) { + width: 90%; + } + + & > * { + width: 100%; + border-radius: 4px; + background: var(--light); + box-shadow: 0 14px 50px rgba(27, 33, 48, 0.12), 0 10px 30px rgba(27, 33, 48, 0.16); + margin-bottom: 2em; + } + + & > input { + box-sizing: border-box; + padding: 0.5em 1em; + font-family: Inter, sans-serif; + color: var(--dark); + font-size: 1.1em; + border: 1px solid var(--outlinegray); + + &:focus { + outline: none; + } + } + + & > #results-container { + & .result-card { + padding: 1em; + cursor: pointer; + transition: background 0.2s ease; + border: 1px solid var(--outlinegray); + border-bottom: none; + width: 100%; + + // normalize button props + font-family: inherit; + font-size: 100%; + line-height: 1.15; + margin: 0; + overflow: visible; + text-transform: none; + text-align: left; + background: var(--light); + outline: none; + + &:hover, &:focus { + background: rgba(180, 180, 180, 0.15); + } + + &:first-of-type { + border-top-left-radius: 5px; + border-top-right-radius: 5px; + } + + &:last-of-type { + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + border-bottom: 1px solid var(--outlinegray); + } + + & > h3, & > p { + margin: 0; + } + } + } + } +} + +.search-highlight { + background-color: #afbfc966; + padding: 0.05em 0.2em; + border-radius: 3px; +} + +.section-ul { + list-style: none; + padding-left: 0; + + & > li { + border: 1px solid var(--outlinegray); + border-radius: 5px; + padding: 0 1em; + margin-bottom: 1em; + + & h3 { + opacity: 1; + font-weight: 700; + margin-bottom: 0em; + } + + & .meta { + opacity: 0.6; + } + } +} + +@keyframes dropin { + 0% { + display: none; + opacity: 0; + visibility: hidden; + } + 1% { + display: inline-block; + opacity: 0; + transform: translate(-50%, 40%); + } + 100% { + opacity: 1; + visibility: visible; + transform: translate(-50%, 20%); + } +} + +.popover { + z-index: 999; + position: absolute; + width: 20em; + display: none; + background-color: var(--light); + padding: 1em; + border: 1px solid var(--outlinegray); + border-radius: 5px; + transform: translate(-50%, 40%); + pointer-events: none; + transition: opacity 0.2s ease, transform 0.2s ease; + user-select: none; + overflow-wrap: anywhere; + box-shadow: 6px 6px 36px 0px rgba(0,0,0,0.25); + + @media all and (max-width: 600px) { + display: none !important; + } + + &.visible { + opacity: 1; + visibility: visible; + transform: translate(-50%, 20%); + display: inline-block; + animation: dropin 0.2s ease; + } + + & > h3 { + font-size: 1rem; + margin: 0.25em 0; + } + + & > .meta { + margin-top: 0.25em; + opacity: 0.5; + font-family: "JetBrains Mono", monospace; + font-size: 0.8rem; + } + + & > p, & > a { + margin: 0; + font-weight: 400; + user-select: none; + } +} + + + +#contact_buttons ul { + list-style-type: none; + + li { + display: inline-block; + } + + li a { + padding: 0 1em; + } +} + + diff --git a/assets/styles/custom.scss b/assets/styles/custom.scss new file mode 100644 index 000000000..54dbacef6 --- /dev/null +++ b/assets/styles/custom.scss @@ -0,0 +1,25 @@ +// Add your own CSS here! +:root { + --light: #faf8f8; + --dark: #141021; + --secondary: #284b63; + --tertiary: #84a59d; + --visited: #afbfc9; + --primary: #f28482; + --gray: #4e4e4e; + --lightgray: #f0f0f0; + --outlinegray: #dadada; + --million-progress-bar-color: var(--secondary); +} + +[saved-theme="dark"] { + --light: #1e1e21 !important; + --dark: #fbfffe !important; + --secondary: #6b879a !important; + --visited: #4a575e !important; + --tertiary: #84a59d !important; + --primary: #f58382 !important; + --gray: #d4d4d4 !important; + --lightgray: #292633 !important; + --outlinegray: #343434 !important; +} \ No newline at end of file diff --git a/config.toml b/config.toml new file mode 100644 index 000000000..803ef1ecb --- /dev/null +++ b/config.toml @@ -0,0 +1,33 @@ +baseURL = "https://quartz.jzhao.xyz/" +languageCode = "en-us" +googleAnalytics = "G-XYFD95KB4J" +pygmentsUseClasses = true +relativeURLs = false +disablePathToLower = true +ignoreFiles = [ + "/content/templates/*", + "/content/private/*", +] +summaryLength = 20 +paginate = 10 +enableGitInfo = true + +[markup] + [markup.tableOfContents] + endLevel = 3 + ordered = true + startLevel = 2 + [markup.highlight] + anchorLineNos = false + codeFences = true + guessSyntax = true + hl_Lines = "" + lineAnchors = "" + lineNoStart = 1 + lineNos = true + lineNumbersInTable = true + style = "dracula" + tabWidth = 4 + [frontmatter] + lastmod = ["lastmod", ":git", "date", "publishDate"] + publishDate = ["publishDate", "date"] diff --git a/data/config.yaml b/data/config.yaml new file mode 100644 index 000000000..cae94ef45 --- /dev/null +++ b/data/config.yaml @@ -0,0 +1,19 @@ +name: Jacky Zhao +enableToc: true +openToc: false +enableLinkPreview: true +enableLatex: true +enableSPA: true +enableFooter: true +enableContextualBacklinks: true +enableRecentNotes: false +description: + Host your second brain and digital garden for free. Quartz features extremely fast full-text search, + Wikilink support, backlinks, local graph, tags, and link previews. +page_title: + "🪴 Quartz 3.2" +links: + - link_name: Twitter + link: https://twitter.com/_jzhao + - link_name: Github + link: https://github.com/jackyzha0 diff --git a/data/graphConfig.yaml b/data/graphConfig.yaml new file mode 100644 index 000000000..a6f916acb --- /dev/null +++ b/data/graphConfig.yaml @@ -0,0 +1,37 @@ +# if true, a Global Graph will be shown on home page with full width, no backlink. +# A different set of Local Graphs will be shown on sub pages. +# if false, Local Graph will be default on every page as usual +enableGlobalGraph: false + +### Local Graph ### + +localGraph: + enableLegend: false + enableDrag: true + enableZoom: true + depth: 1 # set to -1 to show full graph + scale: 1.2 + repelForce: 2 + centerForce: 1 + linkDistance: 1 + fontSize: 0.6 + opacityScale: 3 + +### Global Graph ### + +globalGraph: + enableLegend: false + enableDrag: true + enableZoom: true + depth: -1 # set to -1 to show full graph + scale: 1.4 + repelForce: 1 + centerForce: 1 + linkDistance: 1 + fontSize: 0.5 + opacityScale: 3 + +### For all graphs ### + +paths: + - /moc: "#4388cc" diff --git a/layouts/index.html b/layouts/index.html new file mode 100644 index 000000000..505361420 --- /dev/null +++ b/layouts/index.html @@ -0,0 +1,25 @@ + + +{{ partial "head.html" . }} + + +{{partial "search.html" .}} +
+ +
+

{{if .Title}}{{ .Title }}{{else}}Untitled{{end}}

+ Search IconIcon to open search +
+ {{partial "darkmode.html" .}} +
+
+ {{partial "toc.html" .}} + {{partial "textprocessing.html" . }} + {{if $.Site.Data.config.enableRecentNotes}} + {{partial "recent.html" . }} + {{end}} +
+ {{partial "footerIndex.html" .}} +
+ + diff --git a/layouts/partials/footer.html b/layouts/partials/footer.html new file mode 100644 index 000000000..ddefe75cf --- /dev/null +++ b/layouts/partials/footer.html @@ -0,0 +1,16 @@ + + +
+ +{{if $.Site.Data.config.enableFooter}} +
+ +
+ {{partial "graph.html" .}} +
+
+{{end}} + +{{partial "contact.html" .}} \ No newline at end of file diff --git a/layouts/partials/footerIndex.html b/layouts/partials/footerIndex.html new file mode 100644 index 000000000..5f190446a --- /dev/null +++ b/layouts/partials/footerIndex.html @@ -0,0 +1,24 @@ +{{if $.Site.Data.config.enableFooter}} + {{if $.Site.Data.graphConfig.enableGlobalGraph}} +
+ +
+ {{partial "graph.html" .}} +
+ +
+ {{else}} +
+
+ +
+ {{partial "graph.html" .}} +
+ +
+ {{end}} +{{end}} + +{{partial "contact.html" .}} diff --git a/layouts/partials/head.html b/layouts/partials/head.html new file mode 100644 index 000000000..b3ad28d8c --- /dev/null +++ b/layouts/partials/head.html @@ -0,0 +1,125 @@ + + + + + + {{ if .Title }}{{ .Title }}{{ else }}{{ $.Site.Data.config.page_title }}{{ + end }} + + + + + + + {{$sass := resources.Match "styles/[!_]*.scss" }} + {{$css := slice }} + {{range $sass}} + {{$scss := . | resources.ToCSS (dict "outputStyle" "compressed") }} + {{$css = $css | append $scss}} + {{end}} + {{$finalCss := $css | resources.Concat "styles.css" | resources.Fingerprint "md5" | resources.Minify }} + + + {{ $darkMode := resources.Get "js/darkmode.js" | resources.Fingerprint "md5" | resources.Minify }} + + {{partial "katex.html" .}} + + {{ $popover := resources.Get "js/popover.js" | resources.Fingerprint "md5" | + resources.Minify }} + + + + {{$linkIndex := resources.Get "indices/linkIndex.json" | resources.Fingerprint + "md5" | resources.Minify | }} {{$contentIndex := resources.Get + "indices/contentIndex.json" | resources.Fingerprint "md5" | resources.Minify + }} + + {{if $.Site.Data.config.enableSPA}} + {{ $router := resources.Get "js/router.js" | resources.Fingerprint "md5" | + resources.Minify }} + + {{else}} + + {{end}} + +{{ template "_internal/google_analytics.html" . }} diff --git a/layouts/partials/page-list.html b/layouts/partials/page-list.html new file mode 100644 index 000000000..e51c5ddab --- /dev/null +++ b/layouts/partials/page-list.html @@ -0,0 +1,21 @@ + diff --git a/layouts/partials/recent.html b/layouts/partials/recent.html new file mode 100644 index 000000000..e3926c243 --- /dev/null +++ b/layouts/partials/recent.html @@ -0,0 +1,12 @@ +
+

Recent Notes

+ + {{$notes := .Site.RegularPages}} + {{partial "page-list.html" (first 3 $notes)}} +
+