| .github/workflows/deploy.yaml | ●●●●● patch | view | raw | blame | history | |
| assets/js/popover.js | ●●●●● patch | view | raw | blame | history | |
| assets/js/search.js | ●●●●● patch | view | raw | blame | history | |
| assets/styles/base.scss | ●●●●● patch | view | raw | blame | history | |
| data/config.yaml | ●●●●● patch | view | raw | blame | history | |
| layouts/partials/backlinks.html | ●●●●● patch | view | raw | blame | history | |
| layouts/partials/popover.html | ●●●●● patch | view | raw | blame | history |
.github/workflows/deploy.yaml
@@ -14,7 +14,7 @@ fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod - name: Build Link Index uses: jackyzha0/hugo-obsidian@v2.12 uses: jackyzha0/hugo-obsidian@v2.13 with: index: true input: content assets/js/popover.js
@@ -5,19 +5,20 @@ return template.content.firstChild } function initPopover(baseURL) { function initPopover(baseURL, useContextualBacklinks) { const basePath = baseURL.replace(window.location.origin, "") document.addEventListener("DOMContentLoaded", () => { fetchData.then(({ content }) => { const links = [...document.getElementsByClassName("internal-link")] links .filter(li => li.dataset.src) .filter(li => li.dataset.src || (li.dataset.idx && useContextualBacklinks)) .forEach(li => { const linkDest = content[li.dataset.src.replace(/\/$/g, "").replace(basePath, "")] if (linkDest) { if (li.dataset.ctx) { console.log(li.dataset.ctx) const linkDest = content[li.dataset.src] const popoverElement = `<div class="popover"> <h3>${linkDest.title}</h3> <p>${removeMarkdown(linkDest.content).split(" ", 20).join(" ")}...</p> <p>${highlight(removeMarkdown(linkDest.content), li.dataset.ctx)}...</p> <p class="meta">${new Date(linkDest.lastmodified).toLocaleDateString()}</p> </div>` const el = htmlToElement(popoverElement) @@ -28,6 +29,23 @@ li.addEventListener("mouseout", () => { el.classList.remove("visible") }) } else { const linkDest = content[li.dataset.src.replace(/\/$/g, "").replace(basePath, "")] if (linkDest) { const popoverElement = `<div class="popover"> <h3>${linkDest.title}</h3> <p>${removeMarkdown(linkDest.content).split(" ", 20).join(" ")}...</p> <p class="meta">${new Date(linkDest.lastmodified).toLocaleDateString()}</p> </div>` const el = htmlToElement(popoverElement) li.appendChild(el) li.addEventListener("mouseover", () => { el.classList.add("visible") }) li.addEventListener("mouseout", () => { el.classList.remove("visible") }) } } }) }) assets/js/search.js
@@ -52,9 +52,65 @@ 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(" ")) + `<span class="search-highlight">${term}</span>` + 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 `<span class="search-highlight">${token}</span>` } return token }) .join(' ') .replaceAll('</span> <span class="search-highlight">', ' ') return `${startIndex === 0 ? '' : '...'}${mappedText}${endIndex === splitText.length ? '' : '...' }` }; (async function() { const encoder = (str) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])+/) const contentIndex = new FlexSearch.Document({ @@ -84,52 +140,6 @@ }) } 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 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 `<span class="search-highlight">${token}</span>` } return token }) .join(' ') .replaceAll('</span> <span class="search-highlight">', ' ') return `${startIndex === 0 ? '' : '...'}${mappedText}${endIndex === splitText.length ? '' : '...' }` } const resultToHTML = ({ url, title, content, term }) => { const text = removeMarkdown(content) const resultTitle = highlight(title, term) assets/styles/base.scss
@@ -478,17 +478,17 @@ & > h3, & > p { margin: 0; } & .search-highlight { background-color: #afbfc966; padding: 0.05em 0.2em; border-radius: 3px; } } } } } .search-highlight { background-color: #afbfc966; padding: 0.05em 0.2em; border-radius: 3px; } .section-ul { list-style: none; padding-left: 0; data/config.yaml
@@ -4,6 +4,7 @@ enableLinkPreview: true enableLatex: true enableSPA: false enableContextualBacklinks: true 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. layouts/partials/backlinks.html
@@ -7,13 +7,18 @@ {{$inbound := index $linkIndex.index.backlinks $curPage}} {{$contentTable := getJSON "/assets/indices/contentIndex.json"}} {{if $inbound}} {{$cleanedInbound := apply (apply $inbound "index" "." "source") "replace" "." " " "-"}} {{- range $cleanedInbound | uniq -}} {{$l := printf "%s%s/" $host .}} {{$backlinks := dict "SENTINEL" "SENTINEL"}} {{range $k, $v := $inbound}} {{$cleanedInbound := replace $v.source " " "-"}} {{$ctx := $v.text}} {{$backlinks = merge $backlinks (dict $cleanedInbound $ctx)}} {{end}} {{- range $lnk, $ctx := $backlinks -}} {{$l := printf "%s%s/" $host $lnk}} {{$l = cond (eq $l "//") "/" $l}} {{with (index $contentTable .)}} {{with (index $contentTable $lnk)}} <li> <a href="{{$l}}">{{index (index . "title")}}</a> <a href="{{$l}}" data-ctx="{{$ctx}}" data-src="{{$lnk}}" class="internal-link">{{index (index . "title")}}</a> </li> {{end}} {{- end -}} layouts/partials/popover.html
@@ -2,6 +2,7 @@ {{ $js := resources.Get "js/popover.js" | resources.Fingerprint "md5" | resources.Minify }} <script src="{{ $js.Permalink }}"></script> <script> initPopover({{strings.TrimRight "/" .Site.BaseURL }}) const useContextual = {{ $.Site.Data.config.enableContextualBacklinks }} initPopover({{strings.TrimRight "/" .Site.BaseURL }}, useContextual) </script> {{end}} {{end}}