feat: contextual backlinks (closes #106)
This commit is contained in:
		@@ -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 = `<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 @@ 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 = `<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")
 | 
			
		||||
              })
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
    })
 | 
			
		||||
 
 | 
			
		||||
@@ -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(" ")) + `<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 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 `<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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user