diff --git a/assets/js/graph.js b/assets/js/graph.js index db2a171fd..99ecf54ee 100644 --- a/assets/js/graph.js +++ b/assets/js/graph.js @@ -1,51 +1,72 @@ -async function drawGraph(url, baseUrl, pathColors, depth, enableDrag, enableLegend, enableZoom) { - const { index, links, content } = await fetchData - const curPage = url.replace(baseUrl, "") +async function drawGraph( + url, + baseUrl, + pathColors, + depth, + enableDrag, + enableLegend, + enableZoom +) { + const container = document.getElementById('graph-container'); - const parseIdsFromLinks = (links) => [...(new Set(links.flatMap(link => ([link.source, link.target]))))] + const { index, links, content } = await fetchData; + const curPage = url.replace(baseUrl, ''); - const neighbours = new Set() - const wl = [curPage || "/", "__SENTINEL"] + 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") + 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)) + 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(links).forEach(id => neighbours.add(id)) + parseIdsFromLinks(copyLinks).forEach((id) => neighbours.add(id)); } const data = { - nodes: [...neighbours].map(id => ({ id })), - links: links.filter(l => neighbours.has(l.source) && neighbours.has(l.target)), - } + 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)" + 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] + const path = Object.keys(pathColor)[0]; + const colour = pathColor[path]; if (d.id.startsWith(path)) { - return colour + return colour; } } - return "var(--g-node)" - } + return 'var(--g-node)'; + }; - const drag = simulation => { + const drag = (simulation) => { function dragstarted(event, d) { if (!event.active) simulation.alphaTarget(1).restart(); d.fx = d.x; @@ -63,169 +84,198 @@ async function drawGraph(url, baseUrl, pathColors, depth, enableDrag, enableLege d.fy = null; } - const noop = () => { } - return d3.drag() - .on("start", enableDrag ? dragstarted : noop) - .on("drag", enableDrag ? dragged : noop) - .on("end", enableDrag ? dragended : noop); - } + const noop = () => {}; + return d3 + .drag() + .on('start', enableDrag ? dragstarted : noop) + .on('drag', enableDrag ? dragged : noop) + .on('end', enableDrag ? dragended : noop); + }; - const height = Math.max(document.getElementById("graph-container").offsetHeight, 250) - const width = document.getElementById("graph-container").offsetWidth + const height = Math.max(container.offsetHeight, 250); + const width = container.offsetWidth; - const simulation = d3.forceSimulation(data.nodes) - .force("charge", d3.forceManyBody().strength(-30)) - .force("link", d3.forceLink(data.links).id(d => d.id).distance(40)) - .force("center", d3.forceCenter()); + const simulation = d3 + .forceSimulation(data.nodes) + .force('charge', d3.forceManyBody().strength(-30)) + .force( + 'link', + d3 + .forceLink(data.links) + .id((d) => d.id) + .distance(40) + ) + .force('center', d3.forceCenter()); - const svg = d3.select('#graph-container') + const svg = d3 + .select('#graph-container') .append('svg') .attr('width', width) .attr('height', height) - .attr("viewBox", [-width / 2, -height / 2, width, height]); + .attr('viewBox', [-width / 2, -height / 2, width, height]); if (enableLegend) { const legend = [ - { "Current": "var(--g-node-active)" }, - { "Note": "var(--g-node)" }, - ...pathColors - ] + { 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") - }) + 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") + 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) + .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") + const graphNode = svg + .append('g') + .selectAll('g') .data(data.nodes) - .enter().append("g") + .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 - } + 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) => { - window.location.href = `${baseUrl}/${decodeURI(d.id).replace(/\s+/g, '-')}/` + 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.navigate( + new URL(`${baseUrl}${decodeURI(d.id).replace(/\s+/g, '-')}/`), + '.singlePage' + ); }) - .on("mouseover", function(_, d) { - d3.selectAll(".node") + .on('mouseover', function (_, d) { + d3.selectAll('.node') .transition() .duration(100) - .attr("fill", "var(--g-node-inactive)") + .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 - const linkNodes = d3.selectAll(".link").filter(d => d.source.id === currentId || d.target.id === currentId) + 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; + 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) + neighbourNodes.transition().duration(200).attr('fill', color); // highlight links linkNodes .transition() .duration(200) - .attr("stroke", "var(--g-link-active)") + .attr('stroke', 'var(--g-link-active)'); // show text for self d3.select(this.parentNode) .raise() - .select("text") + .select('text') .transition() .duration(200) - .style("opacity", 1) - .raise() - }).on("mouseleave", function(_, d) { - d3.selectAll(".node") - .transition() - .duration(200) - .attr("fill", color) + .style('opacity', 1); + }) + .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) + 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)") + linkNodes.transition().duration(200).attr('stroke', 'var(--g-link)'); d3.select(this.parentNode) - .select("text") + .select('text') .transition() .duration(200) - .style("opacity", 0) + .style('opacity', 0); }) .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", 0) - .style("pointer-events", "none") - .style("font-size", "0.4em") + 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', 0) + .style('pointer-events', 'none') + .style('font-size', '0.4em') .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 - const scaledOpacity = Math.max((scale - 1) / 3.75, 0) - labels - .attr("transform", transform) - .style("opacity", scaledOpacity) - })); + 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; + const scaledOpacity = Math.max((scale - 1) / 3.75, 0); + labels.attr('transform', transform).style('opacity', scaledOpacity); + }) + ); } // progress the simulation - simulation.on("tick", () => { + 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) + .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/search.js b/assets/js/search.js index 0aacb5f44..fcf79177d 100644 --- a/assets/js/search.js +++ b/assets/js/search.js @@ -9,47 +9,47 @@ const removeMarkdown = ( preserveLinks: false, } ) => { - let output = markdown || ""; - output = output.replace(/^(-\s*?|\*\s*?|_\s*?){3,}\s*$/gm, ""); + 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" + options.listUnicodeChar + ' $1' ); - else output = output.replace(/^([\s\t]*)([\*\-\+]|\d+\.)\s+/gm, "$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, ""); + .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, '$1 ($2)'); } output = output - .replace(/<[^>]*>/g, "") - .replace(/^[=\-]{2,}\s*$/g, "") - .replace(/\[\^.+?\](\: .*?$)?/g, "") - .replace(/\s{0,2}\[.*?\]: .*?$/g, "") - .replace(/\!\[(.*?)\][\[\(].*?[\]\)]/g, options.useImgAltText ? "$1" : "") - .replace(/\[(.*?)\][\[\(].*?[\]\)]/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(/<[^>]*>/g, '') + .replace(/^[=\-]{2,}\s*$/g, '') + .replace(/\[\^.+?\](\: .*?$)?/g, '') + .replace(/\s{0,2}\[.*?\]: .*?$/g, '') + .replace(/\!\[(.*?)\][\[\(].*?[\]\)]/g, options.useImgAltText ? '$1' : '') + .replace(/\[(.*?)\][\[\(].*?[\]\)]/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( /^(\n)?\s{0,}#{1,6}\s+| {0,}(\n)?\s{0,}#{0,} {0,}(\n)?\s{0,}$/gm, - "$1$2$3" + '$1$2$3' ) - .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"); + .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; @@ -58,182 +58,206 @@ const removeMarkdown = ( }; // ----- -(async function() { - const encoder = str => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])+/) +(async function () { + const encoder = (str) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])+/); const contentIndex = new FlexSearch.Document({ cache: true, - charset: "latin:extra", + charset: 'latin:extra', optimize: true, - index: [{ - field: "content", - tokenize: "reverse", - encode: encoder, - }, { - field: "title", - tokenize: "forward", - encode: encoder, - }] - }) + index: [ + { + field: 'content', + tokenize: 'reverse', + encode: encoder, + }, + { + field: 'title', + tokenize: 'forward', + encode: encoder, + }, + ], + }); - const { content } = await fetchData + const { content } = await fetchData; for (const [key, value] of Object.entries(content)) { contentIndex.add({ id: key, title: value.title, content: removeMarkdown(value.content), - }) + }); } const highlight = (content, term) => { - const highlightWindow = 20 - 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 highlightWindow = 20; + 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) + 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) + 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 + bestSum = windowSum; + bestIndex = i; } } - const startIndex = Math.max(bestIndex - highlightWindow, 0) - const endIndex = Math.min(startIndex + 2 * highlightWindow, splitText.length) + const startIndex = Math.max(bestIndex - highlightWindow, 0); + const endIndex = Math.min( + startIndex + 2 * highlightWindow, + splitText.length + ); const mappedText = splitText .slice(startIndex, endIndex) - .map(token => { + .map((token) => { if (includesCheck(token)) { - return `${token}` + return `${token}`; } - return token + return token; }) - .join(" ") - .replaceAll(' ', " ") - return `${startIndex === 0 ? "" : "..."}${mappedText}${endIndex === splitText.length ? "" : "..."}` - } + .join(' ') + .replaceAll(' ', ' '); + return `${startIndex === 0 ? '' : '...'}${mappedText}${ + endIndex === splitText.length ? '' : '...' + }`; + }; const resultToHTML = ({ url, title, content, term }) => { - const text = removeMarkdown(content) - const resultTitle = highlight(title, term) - const resultText = highlight(text, term) + const text = removeMarkdown(content); + const resultTitle = highlight(title, term); + const resultText = highlight(text, term); return `` - } + `; + }; const redir = (id, term) => { - window.location.href = `${BASE_URL}${id}#:~:text=${encodeURIComponent(term)}/` - } + // SPA navigation + window.navigate( + new URL( + `${BASE_URL.slice(0, -1)}${id}#:~:text=${encodeURIComponent(term)}/` + ), + '.singlePage' + ); + closeSearch(); + }; - const formatForDisplay = id => ({ + const formatForDisplay = (id) => ({ id, url: id, title: content[id].title, - content: content[id].content - }) + 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) + 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 + term = e.target.value; const searchResults = contentIndex.search(term, [ { - field: "content", + field: 'content', limit: 10, }, { - field: "title", + field: 'title', limit: 5, - } - ]) - const getByField = field => { - const results = searchResults.filter(x => x.field === field) + }, + ]); + const getByField = (field) => { + const results = searchResults.filter((x) => x.field === field); if (results.length === 0) { - return [] + return []; } else { - return [...results[0].result] + return [...results[0].result]; } - } - const allIds = new Set([...getByField('title'), ...getByField('content')]) - const finalResults = [...allIds].map(formatForDisplay) + }; + 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) - }) + .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") + 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() + if ( + searchContainer.style.display === 'none' || + searchContainer.style.display === '' + ) { + source.value = ''; + results.innerHTML = ''; + searchContainer.style.display = 'block'; + source.focus(); } else { - searchContainer.style.display = "none" + searchContainer.style.display = 'none'; } } function closeSearch() { - searchContainer.style.display = "none" + searchContainer.style.display = 'none'; } document.addEventListener('keydown', (event) => { - if (event.key === "k" && (event.ctrlKey || event.metaKey)) { - event.preventDefault() - openSearch() + if (event.key === 'k' && (event.ctrlKey || event.metaKey)) { + event.preventDefault(); + openSearch(); } - if (event.key === "Escape") { - event.preventDefault() - closeSearch() + if (event.key === 'Escape') { + event.preventDefault(); + closeSearch(); } - }) + }); - const searchButton = document.getElementById("search-icon") + const searchButton = document.getElementById('search-icon'); searchButton.addEventListener('click', (evt) => { - openSearch() - }) + openSearch(); + }); searchButton.addEventListener('keydown', (evt) => { - openSearch() - }) + openSearch(); + }); searchContainer.addEventListener('click', (evt) => { - closeSearch() - }) - document.getElementById("search-space").addEventListener('click', (evt) => { - evt.stopPropagation() - }) -})() - + closeSearch(); + }); + document.getElementById('search-space').addEventListener('click', (evt) => { + evt.stopPropagation(); + }); +})(); diff --git a/layouts/partials/graph.html b/layouts/partials/graph.html index ca379689b..b9f79763c 100644 --- a/layouts/partials/graph.html +++ b/layouts/partials/graph.html @@ -1,25 +1,18 @@ - +

Interactive Graph

{{ $js := resources.Get "js/graph.js" | resources.Fingerprint "md5" }} - diff --git a/layouts/partials/head.html b/layouts/partials/head.html index 195cade44..1dabaf661 100644 --- a/layouts/partials/head.html +++ b/layouts/partials/head.html @@ -1,46 +1,93 @@ - - - - {{ if .Title }}{{ .Title }}{{ else }}{{ $.Site.Data.config.page_title }}{{ end }} - - + + + + + {{ 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 }} - + + + {{$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" .}} + {{ $darkMode := resources.Get "js/darkmode.js" | resources.Fingerprint "md5" | + resources.Minify }} + + {{partial "katex.html" .}} - - {{$linkIndex := resources.Get "indices/linkIndex.json" | resources.Fingerprint "md5" | resources.Minify | }} - {{$contentIndex := resources.Get "indices/contentIndex.json" | 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 + }} + + {{ template "_internal/google_analytics.html" . }}