| | |
| | | tags: string[] |
| | | } |
| | | |
| | | let index: FlexSearch.Document<Item> | undefined = undefined |
| | | |
| | | // Can be expanded with things like "term" in the future |
| | | type SearchType = "basic" | "tags" |
| | | |
| | | // Current searchType |
| | | let searchType: SearchType = "basic" |
| | | // Current search term // TODO: exact match |
| | | let currentSearchTerm: string = "" |
| | | // index for search |
| | | let index: FlexSearch.Document<Item> | undefined = undefined |
| | | |
| | | const contextWindowWords = 30 |
| | | const numSearchResults = 8 |
| | | const numTagResults = 5 |
| | | function highlight(searchTerm: string, text: string, trim?: boolean) { |
| | | // try to highlight longest tokens first |
| | | const tokenizedTerms = searchTerm |
| | | |
| | | const tokenizeTerm = (term: string) => |
| | | term |
| | | .split(/\s+/) |
| | | .filter((t) => t !== "") |
| | | .sort((a, b) => b.length - a.length) |
| | | |
| | | function highlight(searchTerm: string, text: string, trim?: boolean) { |
| | | // try to highlight longest tokens first |
| | | const tokenizedTerms = tokenizeTerm(searchTerm) |
| | | let tokenizedText = text.split(/\s+/).filter((t) => t !== "") |
| | | |
| | | let startIndex = 0 |
| | |
| | | } |
| | | return tok |
| | | }) |
| | | .slice(startIndex, endIndex + 1) |
| | | .join(" ") |
| | | |
| | | return `${startIndex === 0 ? "" : "..."}${slice}${ |
| | |
| | | }` |
| | | } |
| | | |
| | | function highlightHTML(searchTerm: string, el: HTMLElement) { |
| | | // try to highlight longest tokens first |
| | | const p = new DOMParser() |
| | | const tokenizedTerms = tokenizeTerm(searchTerm) |
| | | const html = p.parseFromString(el.innerHTML, "text/html") |
| | | |
| | | const createHighlightSpan = (text: string) => { |
| | | const span = document.createElement("span") |
| | | span.className = "highlight" |
| | | span.textContent = text |
| | | return span |
| | | } |
| | | |
| | | const highlightTextNodes = (node: Node) => { |
| | | if (node.nodeType === Node.TEXT_NODE) { |
| | | let nodeText = node.nodeValue || "" |
| | | tokenizedTerms.forEach((term) => { |
| | | const regex = new RegExp(term.toLowerCase(), "gi") |
| | | const matches = nodeText.match(regex) |
| | | const spanContainer = document.createElement("span") |
| | | let lastIndex = 0 |
| | | matches?.forEach((match) => { |
| | | const matchIndex = nodeText.indexOf(match, lastIndex) |
| | | spanContainer.appendChild(document.createTextNode(nodeText.slice(lastIndex, matchIndex))) |
| | | spanContainer.appendChild(createHighlightSpan(match)) |
| | | lastIndex = matchIndex + match.length |
| | | }) |
| | | spanContainer.appendChild(document.createTextNode(nodeText.slice(lastIndex))) |
| | | node.parentNode?.replaceChild(spanContainer, node) |
| | | }) |
| | | } else if (node.nodeType === Node.ELEMENT_NODE) { |
| | | Array.from(node.childNodes).forEach(highlightTextNodes) |
| | | } |
| | | } |
| | | |
| | | highlightTextNodes(html.body) |
| | | return html.body |
| | | } |
| | | |
| | | const p = new DOMParser() |
| | | const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/) |
| | | let prevShortcutHandler: ((e: HTMLElementEventMap["keydown"]) => void) | undefined = undefined |
| | |
| | | |
| | | const enablePreview = searchLayout?.dataset?.preview === "true" |
| | | let preview: HTMLDivElement | undefined = undefined |
| | | let previewInner: HTMLDivElement | undefined = undefined |
| | | const results = document.createElement("div") |
| | | results.id = "results-container" |
| | | results.style.flexBasis = enablePreview ? "30%" : "100%" |
| | |
| | | el.classList.add("focus") |
| | | |
| | | removeAllChildren(preview as HTMLElement) |
| | | const contentDetails = await fetchContent(slug) |
| | | |
| | | const previewInner = document.createElement("div") |
| | | previewInner = document.createElement("div") |
| | | previewInner.classList.add("preview-inner") |
| | | preview?.appendChild(previewInner) |
| | | contentDetails?.forEach((elt) => previewInner.appendChild(elt)) |
| | | |
| | | const innerDiv = await fetchContent(slug).then((contents) => |
| | | contents.map((el) => highlightHTML(currentSearchTerm, el as HTMLElement)), |
| | | ) |
| | | previewInner.append(...innerDiv) |
| | | } |
| | | |
| | | async function onType(e: HTMLElementEventMap["input"]) { |
| | | let term = (e.target as HTMLInputElement).value |
| | | let searchResults: FlexSearch.SimpleDocumentSearchResultSetUnit[] |
| | | currentSearchTerm = (e.target as HTMLInputElement).value |
| | | |
| | | if (searchLayout) { |
| | | searchLayout.style.opacity = "1" |