mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-24 05:05:20 -04:00
(search) highlight found search terms
This commit is contained in:
parent
e8f5f22ba7
commit
9d15cb1672
@ -31,7 +31,7 @@ const searchInput = useTemplateRef('searchInput');
|
|||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
<li v-for="result of results.data.value" :key="result.id" class="list-group-item">
|
<li v-for="result of results.data.value" :key="result.url" class="list-group-item">
|
||||||
<nuxt-link :to="result.url" class="d-flex text-dark">
|
<nuxt-link :to="result.url" class="d-flex text-dark">
|
||||||
<div class="col-auto pt-1 pe-3">
|
<div class="col-auto pt-1 pe-3">
|
||||||
<Icon v="pen-nib" class="h3" />
|
<Icon v="pen-nib" class="h3" />
|
||||||
@ -46,7 +46,8 @@ const searchInput = useTemplateRef('searchInput');
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col ps-2">
|
<div class="col ps-2">
|
||||||
<Spelling class="h4" :text="result.title" />
|
<Spelling class="d-block h4" :text="result.title" />
|
||||||
|
<Spelling :text="result.fragment" />
|
||||||
<ul class="list-inline mb-0 small">
|
<ul class="list-inline mb-0 small">
|
||||||
<li class="list-inline-item small">
|
<li class="list-inline-item small">
|
||||||
<Icon v="calendar" />
|
<Icon v="calendar" />
|
||||||
|
@ -3,6 +3,7 @@ import fs from 'node:fs/promises';
|
|||||||
import { JSDOM } from 'jsdom';
|
import { JSDOM } from 'jsdom';
|
||||||
import marked from 'marked';
|
import marked from 'marked';
|
||||||
import MiniSearch from 'minisearch';
|
import MiniSearch from 'minisearch';
|
||||||
|
import type { MatchInfo } from 'minisearch';
|
||||||
|
|
||||||
import { getPosts, type PostMetadata } from '~/server/blog.ts';
|
import { getPosts, type PostMetadata } from '~/server/blog.ts';
|
||||||
import { loadSuml, loadSumlFromBase } from '~/server/loader.ts';
|
import { loadSuml, loadSumlFromBase } from '~/server/loader.ts';
|
||||||
@ -21,6 +22,41 @@ interface SearchDocumentPost extends PostMetadata {
|
|||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getTermsByField = (matches: MatchInfo): Record<string, string[]> => {
|
||||||
|
const termsByField: Record<string, string[]> = {};
|
||||||
|
for (const [term, fields] of Object.entries(matches)) {
|
||||||
|
if (term.length === 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const field of fields) {
|
||||||
|
if (!Object.hasOwn(termsByField, field)) {
|
||||||
|
termsByField[field] = [];
|
||||||
|
}
|
||||||
|
termsByField[field].push(term);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return termsByField;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FRAGMENT_MAX_WORDCOUNT = 24;
|
||||||
|
|
||||||
|
const highlightMatches = (field: string, terms: string[] | undefined, fragment: boolean = false): string => {
|
||||||
|
const termsRegex = terms && terms.length > 0 ? new RegExp(`\\b(${terms.join('|')})\\b`, 'ig') : undefined;
|
||||||
|
|
||||||
|
if (fragment) {
|
||||||
|
const words = field.split(' ');
|
||||||
|
const firstMatch = termsRegex !== undefined ? words.findIndex((word) => word.match(termsRegex)) : 0;
|
||||||
|
const start = Math.max(Math.min(firstMatch - 2, words.length - FRAGMENT_MAX_WORDCOUNT), 0);
|
||||||
|
const end = Math.min(start + FRAGMENT_MAX_WORDCOUNT, words.length);
|
||||||
|
field = `${start > 0 ? '[…] ' : ''}${words.slice(start, end).join(' ')}${end < words.length ? ' […]' : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (termsRegex === undefined) {
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
return field.replaceAll(termsRegex, `<mark>$1</mark>`);
|
||||||
|
};
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const index = new MiniSearch<SearchDocumentPost>({
|
const index = new MiniSearch<SearchDocumentPost>({
|
||||||
fields: ['url', 'title', 'content'],
|
fields: ['url', 'title', 'content'],
|
||||||
@ -29,7 +65,9 @@ export default defineEventHandler(async (event) => {
|
|||||||
const posts: SearchDocumentPost[] = [];
|
const posts: SearchDocumentPost[] = [];
|
||||||
for (const post of (await getPosts())) {
|
for (const post of (await getPosts())) {
|
||||||
const content = await fs.readFile(`${rootDir}/data/blog/${post.slug}.md`, 'utf-8');
|
const content = await fs.readFile(`${rootDir}/data/blog/${post.slug}.md`, 'utf-8');
|
||||||
const markdown = marked(content);
|
// exclude title, date and author from searchable content
|
||||||
|
const trimmed = content.replace(/^(.+\n+){2}/, '');
|
||||||
|
const markdown = marked(trimmed);
|
||||||
const parsed = await parseMarkdown(markdown, translator);
|
const parsed = await parseMarkdown(markdown, translator);
|
||||||
const text = JSDOM.fragment(parsed.content ?? '').textContent;
|
const text = JSDOM.fragment(parsed.content ?? '').textContent;
|
||||||
if (text !== null && config.links.enabled && config.links.blog) {
|
if (text !== null && config.links.enabled && config.links.blog) {
|
||||||
@ -50,6 +88,16 @@ export default defineEventHandler(async (event) => {
|
|||||||
const text = query.text as string;
|
const text = query.text as string;
|
||||||
const results = index.search(text, { prefix: true, fuzzy: 1 });
|
const results = index.search(text, { prefix: true, fuzzy: 1 });
|
||||||
return results.map((result) => {
|
return results.map((result) => {
|
||||||
return posts[result.id];
|
const post = posts[result.id];
|
||||||
|
const termsByField = getTermsByField(result.match);
|
||||||
|
return {
|
||||||
|
url: post.url,
|
||||||
|
title: highlightMatches(post.title, termsByField.title),
|
||||||
|
date: post.date,
|
||||||
|
authors: post.authors,
|
||||||
|
hero: post.hero,
|
||||||
|
fragment: highlightMatches(post.content, termsByField.content, true),
|
||||||
|
termsByField,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user