feat: contextual backlinks (closes #106)
This commit is contained in:
parent
6e6dd4cb0b
commit
cea0f3eb74
7 changed files with 101 additions and 66 deletions
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue