forked from FastEndpoints/Documentation
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Replace Algolia search with Flexsearch (FastEndpoints#10)
* Remove the extra dot in the filename extension Caused the generated links to have a dot in the url, e.g. /exception-handler.#unhandled-exception-handler. Make it consistent with the rest of the files so that the logic for generating search hrefs remains consistent and simple. * Ignore JetBrains .idea directory * Add marked, svelte-spotlight and flexsearch dependencies marked - markdown parsing/rendering svelte-spotlight - search results in a modal component flexsearch - client-side search indexing * Add TS type definitions for flexsearch Because both the type definitions that ship with flexsearch and the definitions in @types/flexsearch are incorrect * Add build-time search file generation Served via /search.json * Replace Algolia search with custom flexsearch implementation Fetching of the search file and the indexing/search logic happens in the background, on a SharedWorker to avoid blocking the main browser thread and allowing the page to load before the whole search file is fetched. The SharedWorker is instantiated alongside the store because it makes it easy to manage the worker lifecycle. * Replace yellow mark with light blue
- Loading branch information
Showing
17 changed files
with
991 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -114,3 +114,6 @@ node_modules | |
|
||
# Project specific | ||
/docs | ||
|
||
# JetBrains .idea directory | ||
.idea/ |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
<div | ||
class="cursor-pointer w-full items-center rounded-sm border border-gray-divider bg-gray-elevate py-2.5 px-3 text-[15px] shadow-sm flex gap-x-2" | ||
on:click> | ||
<div class="flex-1 flex items-center"> | ||
<svg width="20" height="20" class="search-icon" viewBox="0 0 20 20"> | ||
<path | ||
d="M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z" | ||
stroke="currentColor" | ||
fill="none" | ||
fill-rule="evenodd" | ||
stroke-linecap="round" | ||
stroke-linejoin="round" | ||
/> | ||
</svg> | ||
<span>Search</span> | ||
</div> | ||
|
||
<span class="flex space-x-0.5 font-semibold"> | ||
<span>⌘</span> | ||
<span>K</span> | ||
</span> | ||
</div> | ||
|
||
<style> | ||
.search-icon { | ||
margin-top: -0.25rem; | ||
margin-right: 0.5rem; | ||
stroke-width: 1.6; | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
<script lang="ts"> | ||
import SvelteSpotlight from 'svelte-spotlight/SvelteSpotlight.svelte'; | ||
import { createEventDispatcher } from 'svelte'; | ||
import type { SearchResult } from '../../types'; | ||
export let query = ""; | ||
export let results : SearchResult[] = []; | ||
export let isOpen = false; | ||
const dispatch = createEventDispatcher(); | ||
$: onOpenChange(isOpen); | ||
$: onQueryChange(query); | ||
function onOpenChange(newIsOpen: boolean) { | ||
dispatch('openchange', { newIsOpen }) | ||
} | ||
function onQueryChange(newQuery: string) { | ||
dispatch('querychange', { newQuery }); | ||
} | ||
function escape(text: string) { | ||
return text.replace(/</g, '<').replace(/>/g, '>'); | ||
} | ||
function excerpt(content: string, query: string) { | ||
const index = content.toLowerCase().indexOf(query.toLowerCase()); | ||
if (index === -1) { | ||
return content.slice(0, 100); | ||
} | ||
const prefix = index > 20 | ||
? `...${content.slice(index - 15, index)}` | ||
: content.slice(0, index); | ||
const suffix = content.slice( | ||
index + query.length, | ||
index + query.length + (80 - (prefix.length + query.length)) | ||
); | ||
return ( | ||
escape(prefix) + | ||
`<mark>${escape(content.slice(index, index + query.length))}</mark>` + | ||
escape(suffix) | ||
); | ||
} | ||
function onKeyboardSelect(event: CustomEvent<SearchResult>) { | ||
dispatch('select', { href: event.detail.href }); | ||
} | ||
</script> | ||
|
||
<SvelteSpotlight | ||
{results} | ||
bind:query | ||
bind:isOpen | ||
modalClass={'w-[600px] max-w-[90%] bg-feDarkBlue-600 shadow-lg rounded-lg'} | ||
headerClass={'py-3 px-10 border-b-2 border-gray-500 border-b-solid'} | ||
inputClass="text-gray-50 focus:outline-none bg-transparent" | ||
contentClass="py-3 [&_li]:py-2" | ||
resultIdKey="href" | ||
on:select={(event) => { | ||
onKeyboardSelect(event); | ||
}} | ||
> | ||
<div | ||
slot="result" | ||
let:selected | ||
let:result | ||
class={`[&_mark]:bg-feLightBlue-600 cursor-pointer hover:bg-feDarkBlue-800 text-sm w-full ${selected ? "bg-feDarkBlue-500" : ''} `} | ||
> | ||
<a href={result.href} | ||
class="ml-10" | ||
on:click={_ => dispatch('select', { href: result.href })} | ||
> | ||
<strong>{@html excerpt(result.title, query)}</strong> | ||
{#if result.content} | ||
<div class="mx-10 text-slate-500 text-sm truncate">{@html excerpt(result.content, query)}</div> | ||
{/if} | ||
</a> | ||
</div> | ||
|
||
<div slot="noResults" class="ml-10 text-slate-500 text-sm"> | ||
No results... | ||
</div> | ||
</SvelteSpotlight> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export { default as SearchModal } from './components/SearchModal.svelte'; | ||
export { default as SearchBox } from './components/SearchBox.svelte'; | ||
export { default as searchStore } from './store'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import Index from "flexsearch"; | ||
import type { SearchBlocks, SearchResult } from "../types"; | ||
|
||
|
||
|
||
const index = new Index<string>({ tokenize: 'forward' }); | ||
const searchLookup = new Map<string, SearchResult>(); | ||
|
||
export let isInited = false; | ||
export let isInitInProgess = false; | ||
|
||
|
||
export async function init(origin: string) { | ||
if (isInited) return; | ||
|
||
isInitInProgess = true; | ||
|
||
const rsp = await fetch(`${origin}/search.json`); | ||
const searchData : SearchBlocks = await rsp.json(); | ||
|
||
for (const { breadcrumbs, content, href } of searchData.blocks) { | ||
const title = breadcrumbs[breadcrumbs.length - 1]; | ||
|
||
searchLookup.set(href, { | ||
title, | ||
href, | ||
breadcrumbs, | ||
content | ||
}); | ||
|
||
index.add(href, `${title} ${content}`); | ||
} | ||
|
||
isInitInProgess = false; | ||
isInited = true; | ||
} | ||
|
||
export function search(query: string): SearchResult[] { | ||
const resultHrefs = index.search(query); | ||
|
||
return resultHrefs.map(lookup); | ||
} | ||
|
||
function lookup(href: string) { | ||
const result = searchLookup.get(href); | ||
|
||
if (result) { | ||
return result; | ||
} | ||
|
||
// Should never reach this state, | ||
// consider disabling @typescript-eslint/no-non-null-assertion | ||
// eslint rule | ||
throw Error("Invalid id/href"); | ||
} | ||
|
||
|
||
|
Oops, something went wrong.