From 113340b230a2de9a87b832a2be1616f7440db519 Mon Sep 17 00:00:00 2001 From: sanyuan <494130947@qq.com> Date: Thu, 6 Oct 2022 15:08:06 +0800 Subject: [PATCH] feat: add header group show --- src/node/plugin.ts | 5 -- .../components/Search/Suggestion.tsx | 61 +++++++++++++++++++ src/theme-default/components/Search/index.tsx | 61 +------------------ src/theme-default/logic/index.ts | 29 +-------- src/theme-default/logic/search.ts | 21 +++++-- src/theme-default/logic/utils.test.ts | 21 +++++++ src/theme-default/logic/utils.ts | 51 ++++++++++++++++ 7 files changed, 150 insertions(+), 99 deletions(-) create mode 100644 src/theme-default/components/Search/Suggestion.tsx create mode 100644 src/theme-default/logic/utils.test.ts create mode 100644 src/theme-default/logic/utils.ts diff --git a/src/node/plugin.ts b/src/node/plugin.ts index c51d1444..67006aa0 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -9,7 +9,6 @@ import babelPluginIsland from './babel-plugin-island'; import { ISLAND_JSX_RUNTIME_PATH } from './constants/index'; import pluginUnocss from 'unocss/vite'; import unocssOptions from './unocss.config'; -// import pluginInspect from 'vite-plugin-inspect'; export async function createIslandPlugins( config: SiteConfig, @@ -18,10 +17,6 @@ export async function createIslandPlugins( ): Promise { return [ pluginUnocss(unocssOptions), - // pluginInspect({ - // dev: false, - // build: true - // }), // Md(x) compile await pluginMdx(config, isServer), // For island internal use diff --git a/src/theme-default/components/Search/Suggestion.tsx b/src/theme-default/components/Search/Suggestion.tsx new file mode 100644 index 00000000..6d74632c --- /dev/null +++ b/src/theme-default/components/Search/Suggestion.tsx @@ -0,0 +1,61 @@ +import type { MatchResultItem } from '../../logic/search'; + +export function SuggestionContent(props: { + suggestion: MatchResultItem; + query: string; + isCurrent: boolean; +}) { + const { suggestion, query } = props; + const renderHeaderMatch = () => { + if (suggestion.type === 'header') { + const { header, headerHighlightIndex } = suggestion; + const headerPrefix = header.slice(0, headerHighlightIndex); + const headerSuffix = header.slice(headerHighlightIndex + query.length); + return ( +
+ {headerPrefix} + + {query} + + {headerSuffix} +
+ ); + } else { + return
{suggestion.header}
; + } + }; + const renderStatementMatch = () => { + if (suggestion.type !== 'content') { + return; + } + const { statementHighlightIndex, statement } = suggestion; + const statementPrefix = statement.slice(0, statementHighlightIndex); + const statementSuffix = statement.slice( + statementHighlightIndex + query.length + ); + return ( +
+ {statementPrefix} + + {query} + + {statementSuffix} +
+ ); + }; + return ( +
+
+ {renderHeaderMatch()} +
+ {suggestion.type === 'content' && renderStatementMatch()} +
+ ); +} diff --git a/src/theme-default/components/Search/index.tsx b/src/theme-default/components/Search/index.tsx index 6a8d9a69..67f0f3bf 100644 --- a/src/theme-default/components/Search/index.tsx +++ b/src/theme-default/components/Search/index.tsx @@ -4,6 +4,7 @@ import { ComponentPropsWithIsland } from '../../../shared/types/index'; import SearchSvg from './icons/search.svg'; import LoadingSvg from './icons/loading.svg'; import { throttle } from 'lodash-es'; +import { SuggestionContent } from './Suggestion'; const KEY_CODE = { ARROW_UP: 'ArrowUp', @@ -11,66 +12,6 @@ const KEY_CODE = { ENTER: 'Enter' }; -function SuggestionContent(props: { - suggestion: MatchResultItem; - query: string; - isCurrent: boolean; -}) { - const { suggestion, query } = props; - const renderHeaderMatch = () => { - if (suggestion.type === 'header') { - const { header, headerHighlightIndex } = suggestion; - const headerPrefix = header.slice(0, headerHighlightIndex); - const headerSuffix = header.slice(headerHighlightIndex + query.length); - return ( -
- {headerPrefix} - - {query} - - {headerSuffix} -
- ); - } else { - return
{suggestion.header}
; - } - }; - const renderStatementMatch = () => { - if (suggestion.type !== 'content') { - return; - } - const { statementHighlightIndex, statement } = suggestion; - const statementPrefix = statement.slice(0, statementHighlightIndex); - const statementSuffix = statement.slice( - statementHighlightIndex + query.length - ); - return ( -
- {statementPrefix} - - {query} - - {statementSuffix} -
- ); - }; - return ( -
-
- {renderHeaderMatch()} -
- {suggestion.type === 'content' && renderStatementMatch()} -
- ); -} - // eslint-disable-next-line @typescript-eslint/no-unused-vars export function Search( props: ComponentPropsWithIsland & { langRoutePrefix: string } diff --git a/src/theme-default/logic/index.ts b/src/theme-default/logic/index.ts index 50d268e0..1dcd1077 100644 --- a/src/theme-default/logic/index.ts +++ b/src/theme-default/logic/index.ts @@ -1,31 +1,3 @@ -export const isProduction = () => import.meta.env.PROD; - -export function addLeadingSlash(url: string) { - return url.charAt(0) === '/' ? url : '/' + url; -} - -export function removeTrailingSlash(url: string) { - return url.charAt(url.length - 1) === '/' ? url.slice(0, -1) : url; -} - -export function normalizeHref(url?: string) { - if (!url) { - return '/'; - } - if (!isProduction() || url.startsWith('http')) { - return url; - } - - let suffix = ''; - if (!import.meta.env.ENABLE_SPA) { - suffix += '.html'; - if (url.endsWith('/')) { - suffix = 'index' + suffix; - } - } - return addLeadingSlash(`${url}${suffix}`); -} - export { usePrevNextPage } from './usePrevNextPage'; export { useEditLink } from './useEditLink'; export { useSidebarData } from './useSidebarData'; @@ -33,3 +5,4 @@ export { useLocaleSiteData } from './useLocaleSiteData'; export { setupEffects, bindingAsideScroll } from './sideEffects'; export { setupCopyCodeButton } from './copyCode'; export { PageSearcher } from './search'; +export * from './utils'; diff --git a/src/theme-default/logic/search.ts b/src/theme-default/logic/search.ts index 7189bd35..b6b38de8 100644 --- a/src/theme-default/logic/search.ts +++ b/src/theme-default/logic/search.ts @@ -3,6 +3,8 @@ import type { Index as SearchIndex, CreateOptions } from 'flexsearch'; import { getAllPages } from 'island/client'; import { uniqBy } from 'lodash-es'; import { normalizeHref } from './index'; +import { Header } from 'shared/types/index'; +import { backTrackHeaders } from './utils'; const THRESHOLD_CONTENT_LENGTH = 100; @@ -11,6 +13,7 @@ interface PageDataForSearch { headers: string[]; content: string; path: string; + rawHeaders: Header[]; } interface CommonMatchResult { @@ -43,7 +46,7 @@ export class PageSearcher { #headerToIdMap: Record = {}; #langRoutePrefix: string; - constructor(langRoutePrefix: string) { + constructor(langRoutePrefix = '') { this.#langRoutePrefix = langRoutePrefix; } @@ -60,7 +63,8 @@ export class PageSearcher { title: page.title!, headers: (page.toc || []).map((header) => header.text), content: page.content || '', - path: page.routePath + path: page.routePath, + rawHeaders: page.toc || [] })); this.#headerToIdMap = pages.reduce((acc, page) => { (page.toc || []).forEach((header) => { @@ -135,15 +139,20 @@ export class PageSearcher { query: string, matchedResult: MatchResultItem[] ): boolean { - const { headers } = item; - for (const header of headers) { + const { headers, rawHeaders } = item; + for (const [index, header] of headers.entries()) { if (header.includes(query)) { const headerAnchor = this.#headerToIdMap[item.path + header]; + // Find the all parent headers (except h1) + // So we can show the full path of the header in search result + // e.g. header2 > header3 > header4 + const headerGroup = backTrackHeaders(rawHeaders, index); + const headerStr = headerGroup.map((item) => item.text).join(' > '); matchedResult.push({ type: 'header', title: item.title, - header, - headerHighlightIndex: header.indexOf(query), + header: headerStr, + headerHighlightIndex: headerStr.indexOf(query), link: `${normalizeHref(item.path)}#${headerAnchor}` }); return true; diff --git a/src/theme-default/logic/utils.test.ts b/src/theme-default/logic/utils.test.ts new file mode 100644 index 00000000..ab3feeea --- /dev/null +++ b/src/theme-default/logic/utils.test.ts @@ -0,0 +1,21 @@ +import { expect, test, describe } from 'vitest'; +import { Header } from 'shared/types'; +import { backTrackHeaders } from './utils'; + +describe('utils logic', () => { + test('back track the headers', () => { + const headers: Header[] = [ + { depth: 1, text: '1', id: '1' }, + { depth: 2, text: '2', id: '2' }, + { depth: 3, text: '3', id: '3' }, + { depth: 4, text: '4', id: '4' }, + { depth: 5, text: '5', id: '5' } + ]; + const res = backTrackHeaders(headers, 3); + expect(res).toEqual([ + { depth: 2, text: '2', id: '2' }, + { depth: 3, text: '3', id: '3' }, + { depth: 4, text: '4', id: '4' } + ]); + }); +}); diff --git a/src/theme-default/logic/utils.ts b/src/theme-default/logic/utils.ts new file mode 100644 index 00000000..b9a61a07 --- /dev/null +++ b/src/theme-default/logic/utils.ts @@ -0,0 +1,51 @@ +import { Header } from 'shared/types'; + +export const isProduction = () => import.meta.env.PROD; + +export function addLeadingSlash(url: string) { + return url.charAt(0) === '/' ? url : '/' + url; +} + +export function removeTrailingSlash(url: string) { + return url.charAt(url.length - 1) === '/' ? url.slice(0, -1) : url; +} + +export function normalizeHref(url?: string) { + if (!url) { + return '/'; + } + if (!isProduction() || url.startsWith('http')) { + return url; + } + + let suffix = ''; + if (!import.meta.env.ENABLE_SPA) { + suffix += '.html'; + if (url.endsWith('/')) { + suffix = 'index' + suffix; + } + } + return addLeadingSlash(`${url}${suffix}`); +} + +export function backTrackHeaders( + rawHeaders: Header[], + index: number +): Header[] { + let current = rawHeaders[index]; + let currentIndex = index; + + const res: Header[] = [current]; + while (current && current.depth > 2) { + for (let i = currentIndex - 1; i >= 0; i--) { + const header = rawHeaders[i]; + if (header.depth > 1 && header.depth === current.depth - 1) { + current = header; + currentIndex = i; + res.unshift(current); + break; + } + } + } + return res; +}