aboutsummaryrefslogtreecommitdiff
path: root/themes/hugo-theme-stack/assets/ts/search.tsx
diff options
context:
space:
mode:
authorAlexander Neonxp Kiryukhin <i@neonxp.ru>2024-11-16 19:32:18 +0300
committerAlexander Neonxp Kiryukhin <i@neonxp.ru>2024-11-17 01:28:57 +0300
commit239d68f94c6250276850fbe95eaa6cdd5c38fb26 (patch)
treec576da169afc442f51eae6213ad0ff749ed86589 /themes/hugo-theme-stack/assets/ts/search.tsx
parent8e79098193fd0a8b65305dd8054cf7c424c60bc5 (diff)
Своя тема, полностью всё переделал
Diffstat (limited to 'themes/hugo-theme-stack/assets/ts/search.tsx')
-rw-r--r--themes/hugo-theme-stack/assets/ts/search.tsx333
1 files changed, 0 insertions, 333 deletions
diff --git a/themes/hugo-theme-stack/assets/ts/search.tsx b/themes/hugo-theme-stack/assets/ts/search.tsx
deleted file mode 100644
index 1c81dd1..0000000
--- a/themes/hugo-theme-stack/assets/ts/search.tsx
+++ /dev/null
@@ -1,333 +0,0 @@
-interface pageData {
- title: string,
- date: string,
- permalink: string,
- content: string,
- image?: string,
- preview: string,
- matchCount: number
-}
-
-interface match {
- start: number,
- end: number
-}
-
-/**
- * Escape HTML tags as HTML entities
- * Edited from:
- * @link https://stackoverflow.com/a/5499821
- */
-const tagsToReplace = {
- '&': '&amp;',
- '<': '&lt;',
- '>': '&gt;',
- '"': '&quot;',
- '…': '&hellip;'
-};
-
-function replaceTag(tag) {
- return tagsToReplace[tag] || tag;
-}
-
-function replaceHTMLEnt(str) {
- return str.replace(/[&<>"]/g, replaceTag);
-}
-
-function escapeRegExp(string) {
- return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
-}
-
-class Search {
- private data: pageData[];
- private form: HTMLFormElement;
- private input: HTMLInputElement;
- private list: HTMLDivElement;
- private resultTitle: HTMLHeadElement;
- private resultTitleTemplate: string;
-
- constructor({ form, input, list, resultTitle, resultTitleTemplate }) {
- this.form = form;
- this.input = input;
- this.list = list;
- this.resultTitle = resultTitle;
- this.resultTitleTemplate = resultTitleTemplate;
-
- /// Check if there's already value in the search input
- if (this.input.value.trim() !== '') {
- this.doSearch(this.input.value.split(' '));
- }
- else {
- this.handleQueryString();
- }
-
- this.bindQueryStringChange();
- this.bindSearchForm();
- }
-
- /**
- * Processes search matches
- * @param str original text
- * @param matches array of matches
- * @param ellipsis whether to add ellipsis to the end of each match
- * @param charLimit max length of preview string
- * @param offset how many characters before and after the match to include in preview
- * @returns preview string
- */
- private static processMatches(str: string, matches: match[], ellipsis: boolean = true, charLimit = 140, offset = 20): string {
- matches.sort((a, b) => {
- return a.start - b.start;
- });
-
- let i = 0,
- lastIndex = 0,
- charCount = 0;
-
- const resultArray: string[] = [];
-
- while (i < matches.length) {
- const item = matches[i];
-
- /// item.start >= lastIndex (equal only for the first iteration)
- /// because of the while loop that comes after, iterating over variable j
-
- if (ellipsis && item.start - offset > lastIndex) {
- resultArray.push(`${replaceHTMLEnt(str.substring(lastIndex, lastIndex + offset))} [...] `);
- resultArray.push(`${replaceHTMLEnt(str.substring(item.start - offset, item.start))}`);
- charCount += offset * 2;
- }
- else {
- /// If the match is too close to the end of last match, don't add ellipsis
- resultArray.push(replaceHTMLEnt(str.substring(lastIndex, item.start)));
- charCount += item.start - lastIndex;
- }
-
- let j = i + 1,
- end = item.end;
-
- /// Include as many matches as possible
- /// [item.start, end] is the range of the match
- while (j < matches.length && matches[j].start <= end) {
- end = Math.max(matches[j].end, end);
- ++j;
- }
-
- resultArray.push(`<mark>${replaceHTMLEnt(str.substring(item.start, end))}</mark>`);
- charCount += end - item.start;
-
- i = j;
- lastIndex = end;
-
- if (ellipsis && charCount > charLimit) break;
- }
-
- /// Add the rest of the string
- if (lastIndex < str.length) {
- let end = str.length;
- if (ellipsis) end = Math.min(end, lastIndex + offset);
-
- resultArray.push(`${replaceHTMLEnt(str.substring(lastIndex, end))}`);
-
- if (ellipsis && end != str.length) {
- resultArray.push(` [...]`);
- }
- }
-
- return resultArray.join('');
- }
-
- private async searchKeywords(keywords: string[]) {
- const rawData = await this.getData();
- const results: pageData[] = [];
-
- const regex = new RegExp(keywords.filter((v, index, arr) => {
- arr[index] = escapeRegExp(v);
- return v.trim() !== '';
- }).join('|'), 'gi');
-
- for (const item of rawData) {
- const titleMatches: match[] = [],
- contentMatches: match[] = [];
-
- let result = {
- ...item,
- preview: '',
- matchCount: 0
- }
-
- const contentMatchAll = item.content.matchAll(regex);
- for (const match of Array.from(contentMatchAll)) {
- contentMatches.push({
- start: match.index,
- end: match.index + match[0].length
- });
- }
-
- const titleMatchAll = item.title.matchAll(regex);
- for (const match of Array.from(titleMatchAll)) {
- titleMatches.push({
- start: match.index,
- end: match.index + match[0].length
- });
- }
-
- if (titleMatches.length > 0) result.title = Search.processMatches(result.title, titleMatches, false);
- if (contentMatches.length > 0) {
- result.preview = Search.processMatches(result.content, contentMatches);
- }
- else {
- /// If there are no matches in the content, use the first 140 characters as preview
- result.preview = replaceHTMLEnt(result.content.substring(0, 140));
- }
-
- result.matchCount = titleMatches.length + contentMatches.length;
- if (result.matchCount > 0) results.push(result);
- }
-
- /// Result with more matches appears first
- return results.sort((a, b) => {
- return b.matchCount - a.matchCount;
- });
- }
-
- private async doSearch(keywords: string[]) {
- const startTime = performance.now();
-
- const results = await this.searchKeywords(keywords);
- this.clear();
-
- for (const item of results) {
- this.list.append(Search.render(item));
- }
-
- const endTime = performance.now();
-
- this.resultTitle.innerText = this.generateResultTitle(results.length, ((endTime - startTime) / 1000).toPrecision(1));
- }
-
- private generateResultTitle(resultLen, time) {
- return this.resultTitleTemplate.replace("#PAGES_COUNT", resultLen).replace("#TIME_SECONDS", time);
- }
-
- public async getData() {
- if (!this.data) {
- /// Not fetched yet
- const jsonURL = this.form.dataset.json;
- this.data = await fetch(jsonURL).then(res => res.json());
- const parser = new DOMParser();
-
- for (const item of this.data) {
- item.content = parser.parseFromString(item.content, 'text/html').body.innerText;
- }
- }
-
- return this.data;
- }
-
- private bindSearchForm() {
- let lastSearch = '';
-
- const eventHandler = (e) => {
- e.preventDefault();
- const keywords = this.input.value.trim();
-
- Search.updateQueryString(keywords, true);
-
- if (keywords === '') {
- lastSearch = '';
- return this.clear();
- }
-
- if (lastSearch === keywords) return;
- lastSearch = keywords;
-
- this.doSearch(keywords.split(' '));
- }
-
- this.input.addEventListener('input', eventHandler);
- this.input.addEventListener('compositionend', eventHandler);
- }
-
- private clear() {
- this.list.innerHTML = '';
- this.resultTitle.innerText = '';
- }
-
- private bindQueryStringChange() {
- window.addEventListener('popstate', (e) => {
- this.handleQueryString()
- })
- }
-
- private handleQueryString() {
- const pageURL = new URL(window.location.toString());
- const keywords = pageURL.searchParams.get('keyword');
- this.input.value = keywords;
-
- if (keywords) {
- this.doSearch(keywords.split(' '));
- }
- else {
- this.clear()
- }
- }
-
- private static updateQueryString(keywords: string, replaceState = false) {
- const pageURL = new URL(window.location.toString());
-
- if (keywords === '') {
- pageURL.searchParams.delete('keyword')
- }
- else {
- pageURL.searchParams.set('keyword', keywords);
- }
-
- if (replaceState) {
- window.history.replaceState('', '', pageURL.toString());
- }
- else {
- window.history.pushState('', '', pageURL.toString());
- }
- }
-
- public static render(item: pageData) {
- return <article>
- <a href={item.permalink}>
- <div class="article-details">
- <h2 class="article-title" dangerouslySetInnerHTML={{ __html: item.title }}></h2>
- <section class="article-preview" dangerouslySetInnerHTML={{ __html: item.preview }}></section>
- </div>
- {item.image &&
- <div class="article-image">
- <img src={item.image} loading="lazy" />
- </div>
- }
- </a>
- </article>;
- }
-}
-
-declare global {
- interface Window {
- searchResultTitleTemplate: string;
- }
-}
-
-window.addEventListener('load', () => {
- setTimeout(function () {
- const searchForm = document.querySelector('.search-form') as HTMLFormElement,
- searchInput = searchForm.querySelector('input') as HTMLInputElement,
- searchResultList = document.querySelector('.search-result--list') as HTMLDivElement,
- searchResultTitle = document.querySelector('.search-result--title') as HTMLHeadingElement;
-
- new Search({
- form: searchForm,
- input: searchInput,
- list: searchResultList,
- resultTitle: searchResultTitle,
- resultTitleTemplate: window.searchResultTitleTemplate
- });
- }, 0);
-})
-
-export default Search; \ No newline at end of file