feat: basic flexsearch implementation

This commit is contained in:
Xin 2023-08-04 01:11:31 +01:00
parent b90c2e7737
commit 16a656947b
7 changed files with 139 additions and 6 deletions

86
assets/js/flexsearch.js Normal file
View File

@ -0,0 +1,86 @@
// {{ $searchDataFile := printf "%s.search-data.json" .Language.Lang }}
// {{ $searchData := resources.Get "json/search-data.json" | resources.ExecuteAsTemplate $searchDataFile . | resources.Minify | resources.Fingerprint }}
(function () {
const searchDataURL = '{{ $searchData.Permalink }}';
console.log('searchDataURL', searchDataURL);
const indexConfig = {
tokenize: "full",
cache: 100,
document: {
id: 'id',
store: ['title', 'href', 'section'],
index: ["title", "content"]
},
context: {
resolution: 9,
depth: 2,
bidirectional: true
}
}
window.flexSearchIndex = new FlexSearch.Document(indexConfig);
const input = document.getElementById('search-input');
const results = document.getElementById('search-results');
input.addEventListener('focus', init);
input.addEventListener('keyup', search);
/**
* Initializes the search functionality by adding the necessary event listeners and fetching the search data.
*/
function init() {
input.removeEventListener('focus', init); // init once
fetch(searchDataURL).then(resp => resp.json()).then(pages => {
pages.forEach(page => {
window.flexSearchIndex.add(page);
});
}).then(search);
}
function search() {
console.log('search', input.value);
while (results.firstChild) {
results.removeChild(results.firstChild);
}
if (!input.value) {
return;
}
const searchHits = window.flexSearchIndex.search(input.value, { limit: 5, enrich: true });
showResults(searchHits);
}
function showResults(hits) {
console.log('showResults', hits);
const flatResults = new Map(); // keyed by href to dedupe hits
for (const result of hits.flatMap(r => r.result)) {
if (flatResults.has(result.doc.href)) continue;
flatResults.set(result.doc.href, result.doc);
}
console.log('flatResults', flatResults);
const create = (str) => {
const div = document.createElement('div');
div.innerHTML = str.trim();
return div.firstChild;
}
const fragment = document.createDocumentFragment();
console.log(fragment)
for (const result of flatResults.values()) {
const li = create(`
<li class="mx-2.5 break-words rounded-md contrast-more:border text-gray-800 contrast-more:border-transparent dark:text-gray-300">
<a class="block scroll-m-12 px-2.5 py-2" data-index="0" href="${result.href}">
<div class="text-base font-semibold leading-5">${result.title}</div>
</a>
</li>`);
fragment.appendChild(li);
}
results.appendChild(fragment);
}
})();

View File

@ -0,0 +1,19 @@
{{- $pages := where .Site.Pages "Kind" "in" (slice "page" "section") -}}
{{- $pages = where $pages "Params.excludeSearch" "!=" true -}}
{{- $pages = where $pages "Content" "!=" "" -}}
[
{{ range $index, $page := $pages }}
{{ $pageTitle := $page.LinkTitle | default $page.File.BaseFileName }}
{{ $pageContent := $page.Plain }}
{{ $pageSection := $page.Parent.LinkTitle }}
{{ if gt $index 0}},{{end}} {
"id": {{ $index }},
"href": "{{ $page.Permalink }}",
"title": {{ $pageTitle | jsonify }},
"section": {{ $pageSection | jsonify }},
"content": {{ $page.Plain | jsonify }}
}
{{- end -}}
]

View File

@ -34,5 +34,5 @@ warning: <svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColo
one: <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="-1 0 19 19"><path d="M16.417 9.6A7.917 7.917 0 1 1 8.5 1.683 7.917 7.917 0 0 1 16.417 9.6zM9.666 6.508H8.248L6.09 8.09l.806 1.103 1.222-.945v4.816h1.547z"></path></svg>
cards: <svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 6.878V6a2.25 2.25 0 0 1 2.25-2.25h7.5A2.25 2.25 0 0 1 18 6v.878m-12 0c.235-.083.487-.128.75-.128h10.5c.263 0 .515.045.75.128m-12 0A2.25 2.25 0 0 0 4.5 9v.878m13.5-3A2.25 2.25 0 0 1 19.5 9v.878m0 0a2.246 2.246 0 0 0-.75-.128H5.25c-.263 0-.515.045-.75.128m15 0A2.25 2.25 0 0 1 21 12v6a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 18v-6c0-.98.626-1.813 1.5-2.122"></path></svg>
copy: <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" class="nextra-copy-icon nx-pointer-events-none nx-h-4 nx-w-4"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></rect><path d="M5 15H4C2.89543 15 2 14.1046 2 13V4C2 2.89543 2.89543 2 4 2H13C14.1046 2 15 2.89543 15 4V5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
copy: <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></rect><path d="M5 15H4C2.89543 15 2 14.1046 2 13V4C2 2.89543 2.89543 2 4 2H13C14.1046 2 15 2.89543 15 4V5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
check: <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M11.467 3.727c.289.189.37.576.181.865l-4.25 6.5a.625.625 0 0 1-.944.12l-2.75-2.5a.625.625 0 0 1 .841-.925l2.208 2.007l3.849-5.886a.625.625 0 0 1 .865-.181Z" clip-rule="evenodd"/></svg>

View File

@ -63,6 +63,10 @@ defaultContentLanguage = 'en'
name = 'About'
pageRef = '/about'
weight = 30
[[menu.main]]
name = 'Search'
weight = 35
params = { placeholder = 'Search documentation...' }
[[menu.main]]
name = 'GitHub'
url = 'https://github.com'

View File

@ -3,17 +3,19 @@
<nav class="mx-auto flex items-center justify-end gap-2 h-16 px-6 max-w-[90rem]">
<a class="flex items-center hover:opacity-75 ltr:mr-auto rtl:ml-auto" href="{{ .Site.BaseURL }}">
{{ partial "utils/icon.html" (dict "context" . "name" "hugo" "attributes" "height=20") }}
{{ partial "utils/icon.html" (dict "name" "hugo" "attributes" "height=20") }}
<span class="mx-2 font-extrabold hidden md:inline select-none" title="{{ .Site.Title }}">
{{ .Site.Title }}
</span>
</a>
{{ $currentPage := . }}
{{ range .Site.Menus.main }}
{{ if .Params.icon }}
{{- $currentPage := . -}}
{{- range .Site.Menus.main -}}
{{- if eq .Name "Search" -}}
{{ partial "search.html" (dict "params" .Params) }}
{{- else if .Params.icon -}}
<a class="p-2 text-current" target="_blank" rel="noreferer" href="{{ .URL }}">
{{ partial "utils/icon.html" (dict "context" $currentPage "name" .Params.icon "attributes" "height=24") }}
{{ partial "utils/icon.html" (dict "name" .Params.icon "attributes" "height=24") }}
<span class="sr-only">{{ .Name }}</span>
</a>
{{ else }}

View File

@ -18,6 +18,11 @@
<script src="{{ $tabsJS.RelPermalink }}"></script>
{{ end }}
{{- $searchJSFile := printf "%s.search.js" .Language.Lang }}
{{- $searchJS := resources.Get "js/flexsearch.js" | resources.ExecuteAsTemplate $searchJSFile . | resources.Minify | resources.Fingerprint -}}
<script src="https://cdn.jsdelivr.net/npm/flexsearch@0.7.31/dist/flexsearch.bundle.min.js"></script>
<script defer src="{{ $searchJS.RelPermalink }}"></script>
{{ if .Page.Params.math }}
<!-- TODO: embed katex in the theme -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.css" integrity="sha384-GvrOXuhMATgEsSwCs4smul74iXGOixntILdUW9XmUC6+HX0sLNAK3q71HotJqlAn" crossorigin="anonymous" />

View File

@ -0,0 +1,17 @@
{{- $placeholder := .params.placeholder | default "Search..." -}}
<div id="search-wrapper" class="search-wrapper relative md:w-64 hidden md:inline-block mx-min-w-[200px]">
<div class="relative flex items-center text-gray-900 contrast-more:text-gray-800 dark:text-gray-300 contrast-more:dark:text-gray-300">
<input id="search-input" placeholder="{{ $placeholder }}" class="block w-full appearance-none rounded-lg px-3 py-2 transition-colors text-base leading-tight md:text-sm bg-black/[.05] dark:bg-gray-50/10 focus:bg-white dark:focus:bg-dark placeholder:text-gray-500 dark:placeholder:text-gray-400 contrast-more:border contrast-more:border-current" type="search" value="" spellcheck="false" />
<kbd class="absolute my-1.5 select-none ltr:right-1.5 rtl:left-1.5 h-5 rounded bg-white px-1.5 font-mono text-[10px] font-medium text-gray-500 border dark:border-gray-100/20 dark:bg-dark/50 contrast-more:border-current contrast-more:text-current contrast-more:dark:border-current items-center gap-1 transition-opacity pointer-events-none hidden sm:flex"> <span class="text-xs"></span>K </kbd>
</div>
<div>
<ul
id="search-results"
class="border border-gray-200 bg-white text-gray-100 dark:border-neutral-800 dark:bg-neutral-900 absolute top-full z-20 mt-2 overflow-auto overscroll-contain rounded-xl py-2.5 shadow-xl max-h-[min(calc(50vh-11rem-env(safe-area-inset-bottom)),400px)] md:max-h-[min(calc(100vh-5rem-env(safe-area-inset-bottom)),400px)] inset-x-0 ltr:md:left-auto rtl:md:right-auto contrast-more:border contrast-more:border-gray-900 contrast-more:dark:border-gray-50 w-screen min-h-[100px] max-w-[min(calc(100vw-2rem),calc(100%+20rem))]"
style="transition: max-height 0.2s ease 0s;"
></ul>
</div>
</div>