mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-22 20:24:18 -04:00
(refactor) common SearchDocument type and <SearchItem> component for all search categories
This commit is contained in:
parent
a12b14256d
commit
dfc524e888
67
components/search/SearchItem.vue
Normal file
67
components/search/SearchItem.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import type { SearchDocument } from '~/src/search.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
document: SearchDocument;
|
||||
}>();
|
||||
|
||||
const iconBySearchDocumentType: Record<SearchDocument['type'], string> = {
|
||||
pronoun: 'tags',
|
||||
noun: 'book',
|
||||
source: 'books',
|
||||
link: 'bookmark',
|
||||
faq: 'map-marker-question',
|
||||
blog: 'pen-nib',
|
||||
term: 'flag',
|
||||
inclusive: 'book-heart',
|
||||
};
|
||||
|
||||
const icon = computed(() => {
|
||||
return iconBySearchDocumentType[props.document.type];
|
||||
});
|
||||
|
||||
const searchDocumentTypesWithImage: SearchDocument['type'][] = ['source', 'blog', 'term'];
|
||||
|
||||
const searchDocumentTypeHasImage = computed(() => {
|
||||
return searchDocumentTypesWithImage.includes(props.document.type);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nuxt-link :to="document.url" class="text-dark">
|
||||
<div class="h3">
|
||||
<Icon :v="icon" />
|
||||
<Spelling :text="document.title" />
|
||||
</div>
|
||||
<div v-if="searchDocumentTypeHasImage" class="d-flex">
|
||||
<div class="col-2">
|
||||
<img
|
||||
v-if="document.image"
|
||||
:src="document.image.src"
|
||||
:class="['w-100 pe-2', document.image.class]"
|
||||
:alt="document.image.alt"
|
||||
loading="lazy"
|
||||
>
|
||||
</div>
|
||||
<div class="col">
|
||||
<Spelling :text="document.content" />
|
||||
<ul v-if="document.date || document.authors" class="list-inline mb-0 small">
|
||||
<li class="list-inline-item small">
|
||||
<Icon v="calendar" />
|
||||
{{ document.date }}
|
||||
</li>
|
||||
<li v-for="author in document.authors" :key="author" class="list-inline-item">
|
||||
<span v-if="author.startsWith('@')" class="badge bg-light text-dark border">
|
||||
<Icon v="collective-logo.svg" class="invertible" />
|
||||
{{ author }}
|
||||
</span>
|
||||
<span v-else class="badge bg-light text-dark border">
|
||||
{{ author }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<Spelling v-else :text="document.content" />
|
||||
</nuxt-link>
|
||||
</template>
|
@ -1,45 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SearchResultBlog } from '~/server/api/search.get.ts';
|
||||
|
||||
defineProps<{
|
||||
result: SearchResultBlog;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nuxt-link :to="result.url" class="text-dark">
|
||||
<div class="h3">
|
||||
<Icon v="pen-nib" />
|
||||
<Spelling :text="result.title" />
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<div class="col-2">
|
||||
<img
|
||||
v-if="result.hero"
|
||||
:src="result.hero.src"
|
||||
:class="['w-100 pe-2', result.hero.class]"
|
||||
:alt="result.hero.alt"
|
||||
loading="lazy"
|
||||
>
|
||||
</div>
|
||||
<div class="col">
|
||||
<Spelling :text="result.content" />
|
||||
<ul class="list-inline mb-0 small">
|
||||
<li class="list-inline-item small">
|
||||
<Icon v="calendar" />
|
||||
{{ result.date }}
|
||||
</li>
|
||||
<li v-for="author in result.authors" :key="author" class="list-inline-item">
|
||||
<span v-if="author.startsWith('@')" class="badge bg-light text-dark border">
|
||||
<Icon v="collective-logo.svg" class="invertible" />
|
||||
{{ author }}
|
||||
</span>
|
||||
<span v-else class="badge bg-light text-dark border">
|
||||
{{ author }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
</template>
|
@ -1,17 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SearchResultFaq } from '~/server/api/search.get.ts';
|
||||
|
||||
defineProps<{
|
||||
result: SearchResultFaq;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nuxt-link :to="result.url" class="text-dark">
|
||||
<div class="h3">
|
||||
<Icon v="map-marker-question" />
|
||||
<Spelling :text="result.title" />
|
||||
</div>
|
||||
<Spelling :text="result.content" />
|
||||
</nuxt-link>
|
||||
</template>
|
@ -1,17 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SearchResultInclusive } from '~/server/api/search.get.ts';
|
||||
|
||||
defineProps<{
|
||||
result: SearchResultInclusive;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nuxt-link :to="result.url" class="text-dark">
|
||||
<div class="h3">
|
||||
<Icon v="book-heart" />
|
||||
<Spelling :text="result.title" />
|
||||
</div>
|
||||
<Spelling :text="result.content" />
|
||||
</nuxt-link>
|
||||
</template>
|
@ -1,17 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SearchResultLink } from '~/server/api/search.get.ts';
|
||||
|
||||
defineProps<{
|
||||
result: SearchResultLink;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nuxt-link :to="result.url" class="text-dark">
|
||||
<div class="h3">
|
||||
<Icon v="bookmark" />
|
||||
<Spelling :text="result.title" />
|
||||
</div>
|
||||
<Spelling :text="result.content" />
|
||||
</nuxt-link>
|
||||
</template>
|
@ -1,17 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SearchResultNoun } from '~/server/api/search.get.ts';
|
||||
|
||||
defineProps<{
|
||||
result: SearchResultNoun;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nuxt-link :to="result.url" class="text-dark">
|
||||
<div class="h3">
|
||||
<Icon v="book" />
|
||||
<Spelling :text="result.title" />
|
||||
</div>
|
||||
<Spelling :text="result.content" />
|
||||
</nuxt-link>
|
||||
</template>
|
@ -1,17 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SearchResultPronoun } from '~/server/api/search.get.ts';
|
||||
|
||||
defineProps<{
|
||||
result: SearchResultPronoun;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nuxt-link :to="result.url" class="text-dark">
|
||||
<div class="h3">
|
||||
<Icon v="tags" class="h3" />
|
||||
<strong><Spelling :text="result.short" /></strong><small v-if="result.small">/<Spelling :text="result.small" /></small>
|
||||
</div>
|
||||
<Spelling :text="result.content" />
|
||||
</nuxt-link>
|
||||
</template>
|
@ -1,29 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SearchResultSource } from '~/server/api/search.get.ts';
|
||||
|
||||
defineProps<{
|
||||
result: SearchResultSource;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nuxt-link :to="result.url" class="text-dark">
|
||||
<div class="h3">
|
||||
<Icon v="books" />
|
||||
<Spelling :text="result.title" />
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<div class="col-2">
|
||||
<img
|
||||
v-if="result.image"
|
||||
:src="result.image"
|
||||
class="w-100 pe-2 border rounded-2"
|
||||
loading="lazy"
|
||||
>
|
||||
</div>
|
||||
<div class="col">
|
||||
<Spelling :text="result.content" />
|
||||
</div>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
</template>
|
@ -1,29 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SearchResultTerm } from '~/server/api/search.get.ts';
|
||||
|
||||
defineProps<{
|
||||
result: SearchResultTerm;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nuxt-link :to="result.url" class="text-dark">
|
||||
<div class="h3">
|
||||
<Icon v="flag" />
|
||||
<Spelling :text="result.title" />
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<div class="col-2">
|
||||
<img
|
||||
v-if="result.image"
|
||||
:src="result.image"
|
||||
class="w-100 pe-2"
|
||||
loading="lazy"
|
||||
>
|
||||
</div>
|
||||
<div class="col">
|
||||
<Spelling :text="result.content" />
|
||||
</div>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
</template>
|
@ -31,15 +31,12 @@ const searchInput = useTemplateRef('searchInput');
|
||||
</section>
|
||||
<section>
|
||||
<ul class="list-group">
|
||||
<li v-for="result of results.data.value" :key="`${result.type}-${result.id}`" class="list-group-item">
|
||||
<SearchItemPronoun v-if="result.type === 'pronoun'" :result="result" />
|
||||
<SearchItemNoun v-else-if="result.type === 'noun'" :result="result" />
|
||||
<SearchItemSource v-else-if="result.type === 'source'" :result="result" />
|
||||
<SearchItemLink v-else-if="result.type === 'link'" :result="result" />
|
||||
<SearchItemFaq v-else-if="result.type === 'faq'" :result="result" />
|
||||
<SearchItemBlog v-else-if="result.type === 'blog'" :result="result" />
|
||||
<SearchItemTerm v-else-if="result.type === 'term'" :result="result" />
|
||||
<SearchItemInclusive v-else-if="result.type === 'inclusive'" :result="result" />
|
||||
<li
|
||||
v-for="document of results.data.value"
|
||||
:key="`${document.type}-${document.id}`"
|
||||
class="list-group-item"
|
||||
>
|
||||
<SearchItem :document="document" />
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
@ -6,7 +6,7 @@ import MiniSearch from 'minisearch';
|
||||
import type { MatchInfo, SearchResult } from 'minisearch';
|
||||
|
||||
import type { Config } from '~/locale/config.ts';
|
||||
import { getPosts, type PostMetadata } from '~/server/blog.ts';
|
||||
import { getPosts } from '~/server/blog.ts';
|
||||
import { getInclusiveEntries } from '~/server/express/inclusive.ts';
|
||||
import { getNounEntries } from '~/server/express/nouns.ts';
|
||||
import { getSourcesEntries } from '~/server/express/sources.ts';
|
||||
@ -17,6 +17,7 @@ import { parsePronouns } from '~/src/buildPronoun.ts';
|
||||
import { genders, gendersWithNumerus } from '~/src/classes.ts';
|
||||
import { clearLinkedText, buildImageUrl } from '~/src/helpers.ts';
|
||||
import parseMarkdown from '~/src/parseMarkdown.ts';
|
||||
import type { SearchDocument } from '~/src/search.ts';
|
||||
import { Translator } from '~/src/translator.ts';
|
||||
import { loadTsv } from '~/src/tsv.ts';
|
||||
|
||||
@ -60,14 +61,16 @@ const highlightMatches = (field: string, terms: string[] | undefined, fragment:
|
||||
return field.replaceAll(termsRegex, `<mark>$1</mark>`);
|
||||
};
|
||||
|
||||
abstract class SearchIndex<D, R> {
|
||||
documents: D[];
|
||||
index: MiniSearch<D>;
|
||||
abstract class SearchIndex {
|
||||
documents: SearchDocument[];
|
||||
index: MiniSearch<SearchDocument>;
|
||||
|
||||
protected constructor(fields: (keyof D)[]) {
|
||||
abstract TYPE: SearchDocument['type'];
|
||||
|
||||
constructor() {
|
||||
this.documents = [];
|
||||
this.index = new MiniSearch({
|
||||
fields: fields as string[],
|
||||
fields: ['url', 'title', 'titleSmall', 'content'],
|
||||
storeFields: ['type'],
|
||||
});
|
||||
}
|
||||
@ -77,36 +80,31 @@ abstract class SearchIndex<D, R> {
|
||||
this.index.addAll(this.documents);
|
||||
}
|
||||
|
||||
abstract getDocuments(config: Config): Promise<D[]>;
|
||||
abstract getDocuments(config: Config): Promise<SearchDocument[]>;
|
||||
|
||||
abstract transform(result: SearchResult): R;
|
||||
}
|
||||
|
||||
interface SearchDocumentPronoun {
|
||||
id: number;
|
||||
type: SearchIndexPronoun['TYPE'];
|
||||
url: string;
|
||||
short: string;
|
||||
small: string | undefined;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type SearchResultPronoun = SearchDocumentPronoun;
|
||||
|
||||
class SearchIndexPronoun extends SearchIndex<SearchDocumentPronoun, SearchResultPronoun> {
|
||||
TYPE = 'pronoun' as const;
|
||||
|
||||
constructor() {
|
||||
super(['url', 'short', 'small', 'content']);
|
||||
transform(result: SearchResult): SearchDocument {
|
||||
const document = this.documents[result.id];
|
||||
const termsByField = getTermsByField(result.match);
|
||||
const transformed = structuredClone(document);
|
||||
transformed.title = highlightMatches(document.title, termsByField.title);
|
||||
transformed.content = highlightMatches(document.content, termsByField.content, true);
|
||||
this.transformAdditionalFields(transformed, termsByField);
|
||||
return transformed;
|
||||
}
|
||||
|
||||
async getDocuments(config: Config): Promise<SearchDocumentPronoun[]> {
|
||||
transformAdditionalFields(_transformed: SearchDocument, _termsByField: Record<string, string[]>) {}
|
||||
}
|
||||
|
||||
class SearchIndexPronoun extends SearchIndex {
|
||||
TYPE = 'pronoun' as const;
|
||||
|
||||
async getDocuments(config: Config): Promise<SearchDocument[]> {
|
||||
if (!config.pronouns.enabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const pronouns = parsePronouns(config, loadTsv(`${rootDir}/data/pronouns/pronouns.tsv`));
|
||||
return Object.values(pronouns).map((pronoun, id): SearchDocumentPronoun => {
|
||||
return Object.values(pronouns).map((pronoun, id): SearchDocument => {
|
||||
const description = Array.isArray(pronoun.description)
|
||||
? pronoun.description.join()
|
||||
: pronoun.description;
|
||||
@ -120,45 +118,26 @@ class SearchIndexPronoun extends SearchIndex<SearchDocumentPronoun, SearchResult
|
||||
id,
|
||||
type: this.TYPE,
|
||||
url: `/${encodeURIComponent(pronoun.canonicalName)}`,
|
||||
short: pronoun.name(),
|
||||
small: pronoun.smallForm ? pronoun.getMorpheme(pronoun.smallForm) ?? undefined : undefined,
|
||||
title: pronoun.name(),
|
||||
titleSmall: pronoun.smallForm ? pronoun.getMorpheme(pronoun.smallForm) ?? undefined : undefined,
|
||||
content: `${description}: ${history} ${morphemes}`,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
override transform(result: SearchResult): SearchResultPronoun {
|
||||
const document = this.documents[result.id];
|
||||
const termsByField = getTermsByField(result.match);
|
||||
return {
|
||||
id: document.id,
|
||||
type: document.type,
|
||||
url: document.url,
|
||||
short: highlightMatches(document.short, termsByField.short),
|
||||
small: document.small ? highlightMatches(document.small, termsByField.small) : undefined,
|
||||
content: highlightMatches(document.content, termsByField.content, true),
|
||||
};
|
||||
override transformAdditionalFields(transformed: SearchDocument, termsByField: Record<string, string[]>) {
|
||||
transformed.title = `<strong>${transformed.title}</strong>`;
|
||||
if (transformed.titleSmall) {
|
||||
transformed.title += `/<small>${highlightMatches(transformed.titleSmall, termsByField.titleSmall)}</small>`;
|
||||
delete transformed.titleSmall;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface SearchDocumentNoun {
|
||||
id: number;
|
||||
type: SearchIndexNoun['TYPE'];
|
||||
url: string;
|
||||
title: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type SearchResultNoun = SearchDocumentNoun;
|
||||
|
||||
class SearchIndexNoun extends SearchIndex<SearchDocumentNoun, SearchResultNoun> {
|
||||
class SearchIndexNoun extends SearchIndex {
|
||||
TYPE = 'noun' as const;
|
||||
|
||||
constructor() {
|
||||
super(['url', 'title', 'content']);
|
||||
}
|
||||
|
||||
async getDocuments(config: Config): Promise<SearchDocumentNoun[]> {
|
||||
async getDocuments(config: Config): Promise<SearchDocument[]> {
|
||||
if (!config.nouns.enabled) {
|
||||
return [];
|
||||
}
|
||||
@ -167,7 +146,7 @@ class SearchIndexNoun extends SearchIndex<SearchDocumentNoun, SearchResultNoun>
|
||||
|
||||
const db = useDatabase();
|
||||
const nouns = await getNounEntries(db, () => false);
|
||||
return nouns.map((noun, id): SearchDocumentNoun => {
|
||||
return nouns.map((noun, id): SearchDocument => {
|
||||
const firstWords = genders
|
||||
.filter((gender) => noun[gender])
|
||||
.map((gender) => noun[gender].split('|')[0]);
|
||||
@ -183,39 +162,12 @@ class SearchIndexNoun extends SearchIndex<SearchDocumentNoun, SearchResultNoun>
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
override transform(result: SearchResult): SearchResultNoun {
|
||||
const document = this.documents[result.id];
|
||||
const termsByField = getTermsByField(result.match);
|
||||
return {
|
||||
id: document.id,
|
||||
type: document.type,
|
||||
url: document.url,
|
||||
title: highlightMatches(document.title, termsByField.title),
|
||||
content: highlightMatches(document.content, termsByField.content, true),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface SearchDocumentSource {
|
||||
id: number;
|
||||
type: SearchIndexSource['TYPE'];
|
||||
url: string;
|
||||
title: string;
|
||||
image: string | undefined;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type SearchResultSource = SearchDocumentSource;
|
||||
|
||||
class SearchIndexSource extends SearchIndex<SearchDocumentSource, SearchResultSource> {
|
||||
class SearchIndexSource extends SearchIndex {
|
||||
TYPE = 'source' as const;
|
||||
|
||||
constructor() {
|
||||
super(['url', 'title', 'content']);
|
||||
}
|
||||
|
||||
async getDocuments(config: Config): Promise<SearchDocumentSource[]> {
|
||||
async getDocuments(config: Config): Promise<SearchDocument[]> {
|
||||
if (!config.sources.enabled) {
|
||||
return [];
|
||||
}
|
||||
@ -226,7 +178,7 @@ class SearchIndexSource extends SearchIndex<SearchDocumentSource, SearchResultSo
|
||||
|
||||
const db = useDatabase();
|
||||
const sources = await getSourcesEntries(db, () => false, undefined);
|
||||
return sources.map((source, id): SearchDocumentSource => {
|
||||
return sources.map((source, id): SearchDocument => {
|
||||
let title = '';
|
||||
if (source.author) {
|
||||
title += `${source.author.replace('^', '')} – `;
|
||||
@ -257,44 +209,17 @@ class SearchIndexSource extends SearchIndex<SearchDocumentSource, SearchResultSo
|
||||
type: this.TYPE,
|
||||
url: `/${base}?filter=${source.title}`,
|
||||
title,
|
||||
image,
|
||||
image: image ? { src: image } : undefined,
|
||||
content,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
override transform(result: SearchResult): SearchResultSource {
|
||||
const document = this.documents[result.id];
|
||||
const termsByField = getTermsByField(result.match);
|
||||
return {
|
||||
id: document.id,
|
||||
type: document.type,
|
||||
url: document.url,
|
||||
title: highlightMatches(document.title, termsByField.title),
|
||||
image: document.image,
|
||||
content: highlightMatches(document.content, termsByField.content, true),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface SearchDocumentLink {
|
||||
id: number;
|
||||
type: SearchIndexLink['TYPE'];
|
||||
url: string;
|
||||
title: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type SearchResultLink = SearchDocumentLink;
|
||||
|
||||
class SearchIndexLink extends SearchIndex<SearchDocumentLink, SearchResultLink> {
|
||||
class SearchIndexLink extends SearchIndex {
|
||||
TYPE = 'link' as const;
|
||||
|
||||
constructor() {
|
||||
super(['url', 'title', 'content']);
|
||||
}
|
||||
|
||||
async getDocuments(config: Config): Promise<SearchDocumentLink[]> {
|
||||
async getDocuments(config: Config): Promise<SearchDocument[]> {
|
||||
if (!config.links.enabled) {
|
||||
return [];
|
||||
}
|
||||
@ -310,38 +235,12 @@ class SearchIndexLink extends SearchIndex<SearchDocumentLink, SearchResultLink>
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
override transform(result: SearchResult): SearchResultLink {
|
||||
const document = this.documents[result.id];
|
||||
const termsByField = getTermsByField(result.match);
|
||||
return {
|
||||
id: document.id,
|
||||
type: document.type,
|
||||
url: document.url,
|
||||
title: highlightMatches(document.title, termsByField.title),
|
||||
content: highlightMatches(document.content, termsByField.content, true),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface SearchDocumentFaq {
|
||||
id: number;
|
||||
type: SearchIndexFaq['TYPE'];
|
||||
url: string;
|
||||
title: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type SearchResultFaq = SearchDocumentFaq;
|
||||
|
||||
class SearchIndexFaq extends SearchIndex<SearchDocumentFaq, SearchResultFaq> {
|
||||
class SearchIndexFaq extends SearchIndex {
|
||||
TYPE = 'faq' as const;
|
||||
|
||||
constructor() {
|
||||
super(['url', 'title', 'content']);
|
||||
}
|
||||
|
||||
async getDocuments(config: Config): Promise<SearchDocumentFaq[]> {
|
||||
async getDocuments(config: Config): Promise<SearchDocument[]> {
|
||||
if (!config.faq.enabled) {
|
||||
return [];
|
||||
}
|
||||
@ -357,42 +256,17 @@ class SearchIndexFaq extends SearchIndex<SearchDocumentFaq, SearchResultFaq> {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
override transform(result: SearchResult): SearchResultFaq {
|
||||
const document = this.documents[result.id];
|
||||
const termsByField = getTermsByField(result.match);
|
||||
return {
|
||||
id: document.id,
|
||||
type: document.type,
|
||||
url: document.url,
|
||||
title: highlightMatches(document.title, termsByField.title),
|
||||
content: highlightMatches(document.content, termsByField.content, true),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface SearchDocumentBlog extends PostMetadata {
|
||||
id: number;
|
||||
type: SearchIndexBlog['TYPE'];
|
||||
url: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type SearchResultBlog = SearchDocumentBlog;
|
||||
|
||||
class SearchIndexBlog extends SearchIndex<SearchDocumentBlog, SearchResultBlog> {
|
||||
class SearchIndexBlog extends SearchIndex {
|
||||
TYPE = 'blog' as const;
|
||||
|
||||
constructor() {
|
||||
super(['url', 'title', 'content']);
|
||||
}
|
||||
|
||||
async getDocuments(config: Config): Promise<SearchDocumentBlog[]> {
|
||||
async getDocuments(config: Config): Promise<SearchDocument[]> {
|
||||
if (!config.links.enabled || !config.links.blog) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const documents: SearchDocumentBlog[] = [];
|
||||
const documents: SearchDocument[] = [];
|
||||
for (const post of (await getPosts())) {
|
||||
const content = await fs.readFile(`${rootDir}/data/blog/${post.slug}.md`, 'utf-8');
|
||||
// exclude title, date and author from searchable content
|
||||
@ -408,49 +282,19 @@ class SearchIndexBlog extends SearchIndex<SearchDocumentBlog, SearchResultBlog>
|
||||
title: post.title,
|
||||
date: post.date,
|
||||
authors: post.authors,
|
||||
hero: post.hero,
|
||||
image: post.hero,
|
||||
content: text,
|
||||
});
|
||||
}
|
||||
}
|
||||
return documents;
|
||||
}
|
||||
|
||||
override transform(result: SearchResult): SearchResultBlog {
|
||||
const document = this.documents[result.id];
|
||||
const termsByField = getTermsByField(result.match);
|
||||
return {
|
||||
id: document.id,
|
||||
type: document.type,
|
||||
url: document.url,
|
||||
title: highlightMatches(document.title, termsByField.title),
|
||||
date: document.date,
|
||||
authors: document.authors,
|
||||
hero: document.hero,
|
||||
content: highlightMatches(document.content, termsByField.content, true),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface SearchDocumentTerm {
|
||||
id: number;
|
||||
type: SearchIndexTerm['TYPE'];
|
||||
url: string;
|
||||
title: string;
|
||||
image: string | undefined;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type SearchResultTerm = SearchDocumentTerm;
|
||||
|
||||
class SearchIndexTerm extends SearchIndex<SearchDocumentTerm, SearchResultTerm> {
|
||||
class SearchIndexTerm extends SearchIndex {
|
||||
TYPE = 'term' as const;
|
||||
|
||||
constructor() {
|
||||
super(['url', 'title', 'content']);
|
||||
}
|
||||
|
||||
async getDocuments(config: Config): Promise<SearchDocumentTerm[]> {
|
||||
async getDocuments(config: Config): Promise<SearchDocument[]> {
|
||||
if (!config.terminology.enabled) {
|
||||
return [];
|
||||
}
|
||||
@ -461,7 +305,7 @@ class SearchIndexTerm extends SearchIndex<SearchDocumentTerm, SearchResultTerm>
|
||||
|
||||
const db = useDatabase();
|
||||
const terms = await getTermsEntries(db, () => false);
|
||||
return terms.map((term, id): SearchDocumentTerm => {
|
||||
return terms.map((term, id): SearchDocument => {
|
||||
const title = term.term.replaceAll('|', ', ');
|
||||
|
||||
let content = '';
|
||||
@ -483,44 +327,17 @@ class SearchIndexTerm extends SearchIndex<SearchDocumentTerm, SearchResultTerm>
|
||||
type: this.TYPE,
|
||||
url: `/${base}?filter=${term.key}`,
|
||||
title,
|
||||
image,
|
||||
image: image ? { src: image } : undefined,
|
||||
content,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
override transform(result: SearchResult): SearchResultTerm {
|
||||
const document = this.documents[result.id];
|
||||
const termsByField = getTermsByField(result.match);
|
||||
return {
|
||||
id: document.id,
|
||||
type: document.type,
|
||||
url: document.url,
|
||||
title: highlightMatches(document.title, termsByField.title),
|
||||
image: document.image,
|
||||
content: highlightMatches(document.content, termsByField.content, true),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface SearchDocumentInclusive {
|
||||
id: number;
|
||||
type: SearchIndexInclusive['TYPE'];
|
||||
url: string;
|
||||
title: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type SearchResultInclusive = SearchDocumentInclusive;
|
||||
|
||||
class SearchIndexInclusive extends SearchIndex<SearchDocumentInclusive, SearchResultInclusive> {
|
||||
class SearchIndexInclusive extends SearchIndex {
|
||||
TYPE = 'inclusive' as const;
|
||||
|
||||
constructor() {
|
||||
super(['url', 'title', 'content']);
|
||||
}
|
||||
|
||||
async getDocuments(config: Config): Promise<SearchDocumentInclusive[]> {
|
||||
async getDocuments(config: Config): Promise<SearchDocument[]> {
|
||||
if (!config.inclusive.enabled) {
|
||||
return [];
|
||||
}
|
||||
@ -529,7 +346,7 @@ class SearchIndexInclusive extends SearchIndex<SearchDocumentInclusive, SearchRe
|
||||
|
||||
const db = useDatabase();
|
||||
const inclusiveEntries = await getInclusiveEntries(db, () => false);
|
||||
return inclusiveEntries.map((inclusiveEntry, id): SearchDocumentInclusive => {
|
||||
return inclusiveEntries.map((inclusiveEntry, id): SearchDocument => {
|
||||
const insteadOf = inclusiveEntry.insteadOf.split('|');
|
||||
const say = inclusiveEntry.say?.split('|') ?? [];
|
||||
let content = `${translator.translate('inclusive.insteadOf')}: ${insteadOf.join(', ')}` +
|
||||
@ -548,18 +365,6 @@ class SearchIndexInclusive extends SearchIndex<SearchDocumentInclusive, SearchRe
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
override transform(result: SearchResult): SearchResultInclusive {
|
||||
const document = this.documents[result.id];
|
||||
const termsByField = getTermsByField(result.match);
|
||||
return {
|
||||
id: document.id,
|
||||
type: document.type,
|
||||
url: document.url,
|
||||
title: highlightMatches(document.title, termsByField.title),
|
||||
content: highlightMatches(document.content, termsByField.content, true),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@ -587,6 +392,6 @@ export default defineEventHandler(async (event) => {
|
||||
return resultB.score - resultA.score;
|
||||
})
|
||||
.map((result) => {
|
||||
return indices[result.type as keyof typeof indices].transform(result);
|
||||
return indices[result.type].transform(result);
|
||||
});
|
||||
});
|
||||
|
17
src/search.ts
Normal file
17
src/search.ts
Normal file
@ -0,0 +1,17 @@
|
||||
interface SearchDocumentImage {
|
||||
src: string;
|
||||
alt?: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export interface SearchDocument {
|
||||
id: number;
|
||||
type: 'pronoun' | 'noun' | 'source' | 'link' | 'faq' | 'blog' | 'term' | 'inclusive';
|
||||
url: string;
|
||||
title: string;
|
||||
titleSmall?: string | undefined;
|
||||
image?: SearchDocumentImage | undefined;
|
||||
content: string;
|
||||
date?: string;
|
||||
authors?: string[];
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user