From cea0f3eb743b26db0d5297ab10e229617585fe0c Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 5 May 2022 00:58:50 -0400 Subject: [PATCH] feat: contextual backlinks (closes #106) --- .github/workflows/deploy.yaml | 2 +- assets/js/popover.js | 28 +++++++-- assets/js/search.js | 104 +++++++++++++++++--------------- assets/styles/base.scss | 12 ++-- data/config.yaml | 1 + layouts/partials/backlinks.html | 15 +++-- layouts/partials/popover.html | 5 +- 7 files changed, 101 insertions(+), 66 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index ee51d3e..8334ea2 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -14,7 +14,7 @@ jobs: 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 diff --git a/assets/js/popover.js b/assets/js/popover.js index ee0477e..ea01156 100644 --- a/assets/js/popover.js +++ b/assets/js/popover.js @@ -5,19 +5,20 @@ function htmlToElement(html) { 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 = `

${linkDest.title}

-

${removeMarkdown(linkDest.content).split(" ", 20).join(" ")}...

+

${highlight(removeMarkdown(linkDest.content), li.dataset.ctx)}...

${new Date(linkDest.lastmodified).toLocaleDateString()}

` const el = htmlToElement(popoverElement) @@ -28,6 +29,23 @@ function initPopover(baseURL) { li.addEventListener("mouseout", () => { el.classList.remove("visible") }) + } else { + const linkDest = content[li.dataset.src.replace(/\/$/g, "").replace(basePath, "")] + if (linkDest) { + const popoverElement = `
+

${linkDest.title}

+

${removeMarkdown(linkDest.content).split(" ", 20).join(" ")}...

+

${new Date(linkDest.lastmodified).toLocaleDateString()}

+
` + const el = htmlToElement(popoverElement) + li.appendChild(el) + li.addEventListener("mouseover", () => { + el.classList.add("visible") + }) + li.addEventListener("mouseout", () => { + el.classList.remove("visible") + }) + } } }) }) diff --git a/assets/js/search.js b/assets/js/search.js index f124d58..c5e293c 100644 --- a/assets/js/search.js +++ b/assets/js/search.js @@ -52,9 +52,65 @@ const removeMarkdown = ( 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({ @@ -84,52 +140,6 @@ const removeMarkdown = ( }) } - 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 `${token}` - } - return token - }) - .join(' ') - .replaceAll(' ', ' ') - return `${startIndex === 0 ? '' : '...'}${mappedText}${endIndex === splitText.length ? '' : '...' - }` - } - const resultToHTML = ({ url, title, content, term }) => { const text = removeMarkdown(content) const resultTitle = highlight(title, term) diff --git a/assets/styles/base.scss b/assets/styles/base.scss index 9bbd933..0787470 100644 --- a/assets/styles/base.scss +++ b/assets/styles/base.scss @@ -478,17 +478,17 @@ header { & > 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; diff --git a/data/config.yaml b/data/config.yaml index afa531c..ccf38eb 100644 --- a/data/config.yaml +++ b/data/config.yaml @@ -4,6 +4,7 @@ openToc: false 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. diff --git a/layouts/partials/backlinks.html b/layouts/partials/backlinks.html index e42351a..23c9091 100644 --- a/layouts/partials/backlinks.html +++ b/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)}}
  • - {{index (index . "title")}} + {{index (index . "title")}}
  • {{end}} {{- end -}} diff --git a/layouts/partials/popover.html b/layouts/partials/popover.html index 1d16622..ba1fd03 100644 --- a/layouts/partials/popover.html +++ b/layouts/partials/popover.html @@ -2,6 +2,7 @@ {{ $js := resources.Get "js/popover.js" | resources.Fingerprint "md5" | resources.Minify }} -{{end}} \ No newline at end of file +{{end}}